Skip to content

Commit 4eda5a8

Browse files
authored
Merge pull request #2009 from CentreForDigitalHumanities/feature/collocations-frontend
Collocations frontend
2 parents 7834f68 + 1886bec commit 4eda5a8

7 files changed

Lines changed: 145 additions & 57 deletions

File tree

frontend/src/app/models/ngram.spec.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ describe('NgramParameters', ()=> {
66
let store: RouterStoreService = new SimpleStore() as any;
77
let ngramParameters: NgramParameters;
88
const testState = {
9+
mode: 'ngrams',
910
size: 3,
1011
positions: 'first',
1112
freqCompensation: true,
1213
analysis: 'clean',
1314
maxDocuments: 100,
1415
numberOfNgrams: 20,
1516
} as NgramSettings;
16-
const testParams = {ngramSettings: 's:3,p:first,c:true,a:clean,m:100,n:20'}
17+
const testParams = {ngramSettings: 'o:n,s:3,p:first,c:true,a:clean,m:100,n:20'}
1718

1819
beforeEach(() => {
1920
ngramParameters = new NgramParameters(store);
@@ -31,6 +32,7 @@ describe('NgramParameters', ()=> {
3132

3233
it('should return default values if no relevant route parameter is present', () => {
3334
const defaultSettings = {
35+
mode: 'ngrams',
3436
size: 2,
3537
positions: 'any',
3638
freqCompensation: false,
@@ -40,6 +42,11 @@ describe('NgramParameters', ()=> {
4042
} as NgramSettings;
4143
const state = ngramParameters.storeToState({irrelevant: 'parameter'})
4244
expect(state).toEqual(defaultSettings);
43-
})
45+
});
46+
47+
it('should parse partial parameters', () => {
48+
const partialParams = {ngramSettings: 's:3,p:first,c:true,a:clean,m:100,n:20'};
49+
expect(ngramParameters.storeToState(partialParams)).toEqual(testState);
4450

51+
});
4552
});

frontend/src/app/models/ngram.ts

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ import * as _ from 'lodash';
44
import { StoreSync } from '../store/store-sync';
55
import { Store } from '../store/types';
66

7+
export type NgramMode = 'ngrams' | 'collocates';
8+
79
export interface NgramSettings {
10+
mode: NgramMode,
811
size: number;
9-
positions: string;
12+
positions?: string;
1013
freqCompensation: boolean;
1114
analysis: string;
1215
maxDocuments: number;
@@ -23,38 +26,41 @@ export class NgramParameters extends StoreSync<NgramSettings> {
2326
}
2427

2528
stringifyNgramSettings(state: NgramSettings): string {
26-
return [`s:${state.size}`,`p:${state.positions}`,`c:${state.freqCompensation}`,
27-
`a:${state.analysis}`,`m:${state.maxDocuments}`,`n:${state.numberOfNgrams}`].join(',')
29+
return [
30+
`o:${state.mode == 'collocates' ? 'c' : 'n'}`,
31+
`s:${state.size}`,
32+
`p:${state.positions}`,
33+
`c:${state.freqCompensation}`,
34+
`a:${state.analysis}`,
35+
`m:${state.maxDocuments}`,
36+
`n:${state.numberOfNgrams}`
37+
].join(',')
2838
}
2939

3040
stateToStore(state: NgramSettings): Params {
3141
return { ngramSettings: this.stringifyNgramSettings(state)}
3242
}
3343

3444
storeToState(params: Params): NgramSettings {
35-
if (_.has(params, 'ngramSettings')) {
36-
const stringComponents = params['ngramSettings'].split(',');
37-
return {
38-
size: parseInt(this.findSetting('s', stringComponents), 10),
39-
positions: this.findSetting('p', stringComponents),
40-
freqCompensation: this.findSetting('c', stringComponents) === 'true',
41-
analysis: this.findSetting('a', stringComponents),
42-
maxDocuments: parseInt(this.findSetting('m', stringComponents), 10),
43-
numberOfNgrams: parseInt(this.findSetting('n', stringComponents), 10),
44-
}
45-
}
45+
const parsed = this.parseParamString(_.get(params, 'ngramSettings', ''));
4646
return {
47-
size: 2,
48-
positions: 'any',
49-
freqCompensation: false,
50-
analysis: 'none',
51-
maxDocuments: 50,
52-
numberOfNgrams: 10,
53-
} as NgramSettings;
47+
mode: _.get(parsed, 'o') === 'c' ? 'collocates' : 'ngrams',
48+
size: this.parseInt(_.get(parsed, 's'), 2),
49+
positions: _.get(parsed, 'p', 'any'),
50+
freqCompensation: _.get(parsed, 'c') === 'true',
51+
analysis: _.get(parsed, 'a', 'none'),
52+
maxDocuments: this.parseInt(_.get(parsed, 'm'), 50),
53+
numberOfNgrams: this.parseInt(_.get(parsed, 'n'), 10),
54+
}
55+
}
56+
57+
private parseParamString(value: string): Record<string, string> {
58+
const pairs = value.split(',').map(part => part.split(':', 2))
59+
return _.fromPairs(pairs);
5460
}
5561

56-
findSetting(abbreviation: string, stringComponents: string[]): string | undefined{
57-
const setting = stringComponents.find(s => s[0] === abbreviation);
58-
return setting.split(':')[1];
62+
private parseInt(value: string | undefined, defaultValue: number): number {
63+
const parsed = parseInt(value, 10);
64+
return _.isNaN(parsed) ? defaultValue : parsed;
5965
}
6066
}

frontend/src/app/models/visualization.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,14 @@ export type WordcloudParameters = {
7878
export type NGramRequestParameters = {
7979
corpus_name: string;
8080
field: string;
81+
mode: 'ngrams' | 'collocates',
8182
ngram_size?: number;
8283
term_position?: string;
8384
freq_compensation?: boolean;
8485
subfield?: string;
8586
max_size_per_interval?: number;
8687
number_of_ngrams?: number;
8788
date_field: string;
88-
mode?: 'ngrams' | 'collocates',
8989
} & APIQuery;
9090

9191

frontend/src/app/services/visualization.service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export class VisualizationService {
104104
...query,
105105
corpus_name: corpus.name,
106106
field,
107+
mode: params.mode,
107108
ngram_size: params.size,
108109
term_position: params.positions,
109110
freq_compensation: params.freqCompensation,

frontend/src/app/visualization/ngram/ngram.component.html

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@
33
<div class="columns is-multiline">
44
<div class="column is-one-third">
55
<div class="field">
6-
<label class="label" iaBalloon="search for bigrams (2 words) or trigrams (3 words)"
6+
<label class="label" iaBalloon="view ngrams (phrases) or collocates (loose words)"
77
iaBalloonPosition="up-left" iaBalloonLength="fit"
8-
id="label-size">
9-
Length of n-gram
8+
id="label-mode">
9+
Mode
1010
</label>
1111
<div class="control">
12-
<ia-dropdown [value]="currentSizeOption" labelledBy="label-size"
13-
(onChange)="onParameterChange('size', $event.value)">
14-
<span iaDropdownLabel>{{currentSizeOption?.label}}</span>
12+
<ia-dropdown [value]="currentModeOption" labelledBy="label-mode"
13+
(onChange)="onParameterChange('mode', $event.value)">
14+
<span iaDropdownLabel>{{currentModeOption?.label}}</span>
1515
<div iaDropdownMenu>
16-
<a *ngFor="let option of sizeOptions"
16+
<a *ngFor="let option of modeOptions"
1717
iaDropdownItem [value]="option">
1818
{{option.label}}
1919
</a>
@@ -24,17 +24,21 @@
2424
</div>
2525
<div class="column is-one-third">
2626
<div class="field">
27-
<label class="label" iaBalloon="only search n-grams with the search term in the specified position"
28-
iaBalloonPosition="up-left" iaBalloonLength="fit"
29-
id="label-positions">
30-
Position of search term
27+
<label class="label" iaBalloon="search for bigrams (2 words) or trigrams (3 words)"
28+
iaBalloonPosition="up-left" iaBalloonLength="fit"
29+
id="label-size">
30+
@if (currentSettings.mode === 'ngrams') {
31+
Length of n-gram
32+
} @else {
33+
Maximum distance
34+
}
3135
</label>
3236
<div class="control">
33-
<ia-dropdown [value]="currentPositionsOption" labelledBy="label-positions"
34-
(onChange)="onParameterChange('positions', $event.value)">
35-
<span iaDropdownLabel>{{currentPositionsOption?.label}}</span>
37+
<ia-dropdown [value]="currentSizeOption" labelledBy="label-size"
38+
(onChange)="onParameterChange('size', $event.value)">
39+
<span iaDropdownLabel>{{currentSizeOption?.label}}</span>
3640
<div iaDropdownMenu>
37-
<a *ngFor="let option of positionsOptions"
41+
<a *ngFor="let option of sizeOptions"
3842
iaDropdownItem [value]="option">
3943
{{option.label}}
4044
</a>
@@ -43,6 +47,29 @@
4347
</div>
4448
</div>
4549
</div>
50+
@if (currentSettings.mode === 'ngrams') {
51+
<div class="column is-one-third">
52+
<div class="field">
53+
<label class="label" iaBalloon="only search n-grams with the search term in the specified position"
54+
iaBalloonPosition="up-left" iaBalloonLength="fit"
55+
id="label-positions">
56+
Position of search term
57+
</label>
58+
<div class="control">
59+
<ia-dropdown [value]="currentPositionsOption" labelledBy="label-positions"
60+
(onChange)="onParameterChange('positions', $event.value)">
61+
<span iaDropdownLabel>{{currentPositionsOption?.label}}</span>
62+
<div iaDropdownMenu>
63+
<a *ngFor="let option of positionsOptions"
64+
iaDropdownItem [value]="option">
65+
{{option.label}}
66+
</a>
67+
</div>
68+
</ia-dropdown>
69+
</div>
70+
</div>
71+
</div>
72+
}
4673
<div class="column is-one-third">
4774
<div class="field">
4875
<label class="label" iaBalloon="divide by the average frequency of the words in the n-gram; favours words that are otherwise rare"

frontend/src/app/visualization/ngram/ngram.component.spec.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,17 @@ describe('NgramComponent', () => {
1717
let fixture: ComponentFixture<NgramComponent>;
1818
let apiService: ApiServiceMock;
1919
let visualizationService: VisualizationService;
20-
let cacheKey = 's:2,p:any,c:false,a:none,m:50,n:10';
21-
let defaultSettings = {
20+
let cacheKey = 'o:n,s:2,p:any,c:false,a:none,m:50,n:10';
21+
let defaultSettings: NgramSettings = {
22+
mode: 'ngrams',
2223
size: 2,
2324
positions: 'any',
2425
freqCompensation: false,
2526
analysis: 'none',
2627
maxDocuments: 50,
2728
numberOfNgrams: 10,
28-
} as NgramSettings;
29+
};
30+
let element: HTMLElement;
2931

3032
beforeEach(waitForAsync(() => {
3133
commonTestBed().testingModule.compileComponents();
@@ -56,6 +58,7 @@ describe('NgramComponent', () => {
5658
component.asTable = false;
5759
component.palette = ['yellow', 'blue'];
5860
fixture.detectChanges();
61+
element = fixture.nativeElement;
5962
});
6063

6164
it('should create', () => {
@@ -66,11 +69,31 @@ describe('NgramComponent', () => {
6669
expect(component.ngramParameters.state$.value).toEqual(defaultSettings);
6770
});
6871

72+
it('should switch labels in size selection', () => {
73+
const label = element.querySelector('#label-size');
74+
const dropdownLabel = (label.nextSibling as HTMLElement).querySelector('[iaDropdownLabel]')
75+
expect(label.textContent.trim()).toBe('Length of n-gram');
76+
expect(dropdownLabel.textContent.trim()).toBe('bigrams');
77+
78+
component.onParameterChange('mode', 'collocates');
79+
fixture.detectChanges();
80+
expect(label.textContent.trim()).toBe('Maximum distance');
81+
expect(dropdownLabel.textContent.trim()).toBe('1');
82+
83+
component.onParameterChange('size', 3);
84+
fixture.detectChanges();
85+
expect(dropdownLabel.textContent.trim()).toBe('2');
86+
87+
component.onParameterChange('mode', 'ngrams');
88+
fixture.detectChanges();
89+
expect(dropdownLabel.textContent.trim()).toBe('trigrams');
90+
});
91+
6992
it('should not abort tasks when `onParameterChange` is triggered during initialization', () => {
7093
spyOn(component.stopPolling$, 'next');
7194
component.onParameterChange('size', 2);
7295
expect(component.stopPolling$.next).not.toHaveBeenCalled();
73-
})
96+
});
7497

7598
it('should stop polling and abort running tasks when changing settings', () => {
7699
const dropdown = fixture.debugElement.query(By.css('ia-dropdown'));
@@ -102,6 +125,6 @@ describe('NgramComponent', () => {
102125
component.resultsCache = { [cacheKey]: fakeNgramResult };
103126
component.confirmChanges();
104127
expect(visualizationService.getNgramTasks).not.toHaveBeenCalled();
105-
})
128+
});
106129

107130
});

frontend/src/app/visualization/ngram/ngram.component.ts

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ import {
2020
import { RouterStoreService } from '@app/store/router-store.service';
2121
import { NgramParameters, NgramSettings } from '@models/ngram';
2222

23+
const makeNumberOptions = (values: number[]) => values.map((n) => ({
24+
label: `${n}`,
25+
value: n,
26+
}));
2327

2428
@Component({
2529
selector: 'ia-ngram',
@@ -62,11 +66,26 @@ export class NgramComponent implements OnChanges {
6266
currentResults: NgramResults;
6367

6468
// options
65-
sizeOptions = [
69+
modeOptions = [
70+
{ label: 'ngrams', value: 'ngrams' },
71+
{ label: 'collocates', value: 'collocates' },
72+
];
73+
/** Size options for ngrams mode */
74+
ngramsSizeOptions = [
6675
{ label: 'bigrams', value: 2 },
6776
{ label: 'trigrams', value: 3 },
6877
{ label: 'fourgrams', value: 4 },
6978
];
79+
/** Size options for collocates mode.
80+
* Note: for compatability with ngrams mode, values also count the search term itself,
81+
* so a value of 2 (analogous to a bigram) means a maximum distance of 1.
82+
*/
83+
collocationsSizeOptions = [
84+
{ label: '1', value: 2 },
85+
{ label: '2', value: 3 },
86+
{ label: '3', value: 4 },
87+
{ label: '5', value: 6 },
88+
];
7089
positionsOptions = ['any', 'first', 'second'].map((n) => ({
7190
label: `${n}`,
7291
value: n,
@@ -76,14 +95,8 @@ export class NgramComponent implements OnChanges {
7695
{ label: 'Yes', value: true },
7796
];
7897
analysisOptions: { label: string; value: string }[];
79-
maxDocumentsOptions = [50, 100, 200, 500].map((n) => ({
80-
label: `${n}`,
81-
value: n,
82-
}));
83-
numberOfNgramsOptions = [10, 20, 50, 100].map((n) => ({
84-
label: `${n}`,
85-
value: n,
86-
}));
98+
maxDocumentsOptions = makeNumberOptions([50, 100, 200, 500]);
99+
numberOfNgramsOptions = makeNumberOptions([10, 20, 50, 100]);
87100

88101
tasksToCancel: string[];
89102

@@ -110,10 +123,21 @@ export class NgramComponent implements OnChanges {
110123
this.currentSettings = _.clone(this.ngramParameters.state$.value);
111124
}
112125

126+
get sizeOptions() {
127+
return this.currentSettings.mode == 'ngrams' ? this.ngramsSizeOptions :
128+
this.collocationsSizeOptions;
129+
}
130+
131+
get currentModeOption() {
132+
return this.modeOptions.find(
133+
(item) => item.value === this.currentSettings.mode
134+
);
135+
}
136+
113137
get currentSizeOption() {
114138
return this.sizeOptions.find(
115139
(item) => item.value === this.currentSettings.size
116-
);
140+
) || this.sizeOptions[0];
117141
}
118142

119143
get currentPositionsOption() {

0 commit comments

Comments
 (0)