@datagrok/bio 2.11.29 → 2.11.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,43 +9,73 @@ import * as DG from 'datagrok-api/dg';
9
9
  import * as grok from 'datagrok-api/grok';
10
10
 
11
11
  import wu from 'wu';
12
- import {fromEvent, Subject, Subscription, Unsubscribable} from 'rxjs';
13
- import {debounceTime} from 'rxjs/operators';
12
+ import $ from 'cash-dom';
13
+ import {fromEvent, Observable, Subject, Subscription, Unsubscribable} from 'rxjs';
14
14
 
15
15
  import {TAGS as bioTAGS, NOTATION} from '@datagrok-libraries/bio/src/utils/macromolecule';
16
- import {errInfo, errStack} from '@datagrok-libraries/bio/src/utils/err-info';
17
- import {delay} from '@datagrok-libraries/utils/src/test';
18
- import {IHelmWebEditor} from '@datagrok-libraries/bio/src/types/editor';
16
+ import {errInfo} from '@datagrok-libraries/bio/src/utils/err-info';
17
+ import {delay, testEvent} from '@datagrok-libraries/utils/src/test';
18
+ import {getHelmHelper} from '@datagrok-libraries/bio/src/helm/helm-helper';
19
+ import {IHelmWebEditor, IWebEditorApp} from '@datagrok-libraries/bio/src/helm/types';
20
+ import {UnitsHandler} from '@datagrok-libraries/bio/src/utils/units-handler';
21
+ import {IRenderer} from '@datagrok-libraries/bio/src/types/renderer';
22
+ import {ILogger} from '@datagrok-libraries/bio/src/utils/logger';
23
+ import {PromiseSyncer} from '@datagrok-libraries/bio/src/utils/syncer';
19
24
 
20
25
  import {helmSubstructureSearch, linearSubstructureSearch} from '../substructure-search/substructure-search';
21
26
  import {updateDivInnerHTML} from '../utils/ui-utils';
27
+ import {BioFilterBase, BioFilterProps, IBioFilter, IFilterProps} from './bio-substructure-filter-types';
28
+ import {HelmBioFilter} from './bio-substructure-filter-helm';
22
29
 
23
30
  import {_package} from '../package';
24
31
 
25
- export class BioSubstructureFilter extends DG.Filter {
26
- bioFilter: BioFilterBase | null = null;
32
+ const FILTER_SYNC_EVENT: string = 'bio-substructure-filter';
33
+
34
+ class FilterState {
35
+ constructor(
36
+ public readonly props: IFilterProps,
37
+ public readonly filterId: number,
38
+ public readonly dataFrameId: string,
39
+ public readonly columnName: string,
40
+ public readonly bitset: DG.BitSet | null,
41
+ ) {}
42
+ }
43
+
44
+ export class SeparatorFilterProps extends BioFilterProps {
45
+ constructor(
46
+ substructure: string,
47
+ public separator?: string
48
+ ) {
49
+ super(substructure);
50
+ }
51
+ }
52
+
53
+ export class BioSubstructureFilter extends DG.Filter implements IRenderer {
54
+ bioFilter: IBioFilter | null = null;
27
55
  bitset: DG.BitSet | null = null;
28
- loader: HTMLDivElement = ui.loader();
29
- onBioFilterChangedSubs?: Subscription;
56
+ readonly loader: HTMLDivElement;
30
57
  notation: string | undefined = undefined;
31
58
 
59
+ readonly logger: ILogger;
60
+ readonly filterSyncer: PromiseSyncer;
61
+
32
62
  get calculating(): boolean { return this.loader.style.display == 'initial'; }
33
63
 
34
64
  set calculating(value: boolean) { this.loader.style.display = value ? 'initial' : 'none'; }
35
65
 
36
66
  get filterSummary(): string {
37
- return this.bioFilter!.substructure;
67
+ return this.bioFilter!.filterSummary;
38
68
  }
39
69
 
40
70
  get isFiltering(): boolean {
41
- return super.isFiltering && this.bioFilter!.substructure !== '';
71
+ return super.isFiltering && (this.bioFilter?.isFiltering ?? false);
42
72
  }
43
73
 
44
74
  get isReadyToApplyFilter(): boolean {
45
75
  return !this.calculating && this.bitset != null;
46
76
  }
47
77
 
48
- get _debounceTime(): number {
78
+ get debounceTime(): number {
49
79
  if (this.column == null)
50
80
  return 1000;
51
81
  const length = this.column.length;
@@ -54,44 +84,85 @@ export class BioSubstructureFilter extends DG.Filter {
54
84
  const msecMax = 1000;
55
85
  if (length < minLength) return 0;
56
86
  if (length > maxLength) return msecMax;
57
- return Math.floor(msecMax * ((length - minLength) / (maxLength - minLength)));
87
+ const res = Math.floor(msecMax * ((length - minLength) / (maxLength - minLength)));
88
+ return res;
58
89
  }
59
90
 
60
- //column name setter overload
61
-
62
91
  constructor() {
63
92
  super();
64
93
  this.root = ui.divV([]);
94
+ this.loader = ui.loader();
65
95
  this.calculating = false;
96
+ this.filterSyncer = new PromiseSyncer(this.logger = _package.logger);
97
+
98
+ return new Proxy(this, {
99
+ set(target: any, key, value) {
100
+ if (key === 'column') {
101
+ const k = 42;
102
+ }
103
+ target[key] = value;
104
+ return true;
105
+ }
106
+ });
66
107
  }
67
108
 
109
+ private static filterCounter: number = -1;
110
+ private readonly filterId: number = ++BioSubstructureFilter.filterCounter;
111
+
112
+ private filterToLog(): string { return `BioSubstructureFilter<${this.filterId}>`; }
113
+
114
+
115
+ private viewSubs: Unsubscribable[] = [];
116
+
68
117
  attach(dataFrame: DG.DataFrame): void {
69
- super.attach(dataFrame);
70
- this.column = dataFrame.columns.bySemType(DG.SEMTYPE.MACROMOLECULE);
71
- this.columnName ??= this.column?.name;
72
- this.notation ??= this.column?.getTag(DG.TAGS.UNITS);
73
- this.bioFilter = this.notation === NOTATION.FASTA ?
74
- new FastaFilter() : this.notation === NOTATION.SEPARATOR ?
75
- new SeparatorFilter(this.column!.getTag(bioTAGS.separator)) : new HelmFilter();
76
- this.root.appendChild(this.bioFilter!.filterPanel);
77
- this.root.appendChild(this.loader);
78
-
79
- this.onBioFilterChangedSubs?.unsubscribe();
80
-
81
- let onChangedEvent: any = this.bioFilter.onChanged;
82
- onChangedEvent = onChangedEvent.pipe(debounceTime(this._debounceTime));
83
- this.onBioFilterChangedSubs = onChangedEvent.subscribe(async (_: any) => await this._onInputChanged());
84
-
85
- this.subs.push(grok.events.onResetFilterRequest.subscribe((_value: any) => {
86
- this.bioFilter?.resetFilter();
87
- }));
118
+ const superAttach = super.attach.bind(this);
119
+ const logPrefix = `${this.filterToLog()}.attach()`;
120
+ this.filterSyncer.sync(logPrefix, async () => {
121
+ superAttach(dataFrame);
122
+ this.column = dataFrame.columns.bySemType(DG.SEMTYPE.MACROMOLECULE);
123
+ const uh = UnitsHandler.getOrCreate(this.column!);
124
+ this.columnName ??= this.column?.name;
125
+ this.notation ??= this.column?.getTag(DG.TAGS.UNITS);
126
+
127
+ this.bioFilter = this.notation === NOTATION.FASTA ?
128
+ new FastaBioFilter() : this.notation === NOTATION.SEPARATOR ?
129
+ new SeparatorBioFilter(this.column!.getTag(bioTAGS.separator)) : new HelmBioFilter();
130
+ this.root.appendChild(this.bioFilter!.filterPanel);
131
+ this.root.appendChild(this.loader);
132
+ await this.bioFilter.attach(); // may await waitForElementInDom
133
+
134
+ this.viewSubs.push(DG.debounce(this.bioFilter!.onChanged, this.debounceTime)
135
+ .subscribe(this.bioFilterOnChangedDebounced.bind(this)));
136
+ this.viewSubs.push(grok.events.onResetFilterRequest
137
+ .subscribe((_value: any) => { this.bioFilter?.resetFilter(); }));
138
+ this.viewSubs.push(grok.events.onCustomEvent(FILTER_SYNC_EVENT)
139
+ .subscribe(this.filterOnSync.bind(this)));
140
+ });
88
141
  }
89
142
 
90
143
  detach() {
91
- if (this.bioFilter) this.bioFilter.detach();
92
- super.detach();
144
+ const superDetach = super.detach.bind(this);
145
+ const logPrefix = `${this.filterToLog()}.detach()`;
146
+ this.filterSyncer.sync(logPrefix, async () => {
147
+ for (const sub of this.viewSubs) sub.unsubscribe();
148
+ this.viewSubs = [];
149
+ superDetach(); // requests this.isFiltering
150
+ if (this.bioFilter) this.bioFilter.detach();
151
+ this.bioFilter = null;
152
+ });
93
153
  }
94
154
 
155
+ // -- Sync -
156
+
157
+ private filterOnSync(state: FilterState): void {
158
+ if (state.filterId === this.filterId) return;
159
+ if (state.dataFrameId !== this.dataFrame!.id || state.columnName !== this.columnName) return;
160
+
161
+ this.bioFilter!.props = state.props;
162
+ }
163
+
164
+ // -- Layout --
165
+
95
166
  applyFilter(): void {
96
167
  if (this.bitset && !this.isDetached)
97
168
  this.dataFrame?.filter.and(this.bitset);
@@ -102,7 +173,7 @@ export class BioSubstructureFilter extends DG.Filter {
102
173
  */
103
174
  saveState(): any {
104
175
  const state = super.saveState();
105
- state.bioSubstructure = this.bioFilter?.substructure;
176
+ state.props = this.bioFilter!.props.save();
106
177
  return state;
107
178
  }
108
179
 
@@ -111,105 +182,165 @@ export class BioSubstructureFilter extends DG.Filter {
111
182
  */
112
183
  applyState(state: any): void {
113
184
  super.applyState(state); //column, columnName
114
- if (state.bioSubstructure)
115
- this.bioFilter!.substructure = state.bioSubstructure;
185
+ if (state.props)
186
+ this.bioFilter!.props.apply(state.props);
187
+
188
+ // if (state.bioSubstructure) {
189
+ // // (async () => { await this.bioFilterOnChangedDebounced(); })();
190
+ // this.bioFilter!.substructure = state.bioSubstructure;
191
+ // }
192
+ }
116
193
 
117
- const that = this;
118
- if (state.bioSubstructure)
119
- setTimeout(function() { that._onInputChanged(); }, 1000);
194
+ private fireFilterSync(): void {
195
+ const logPrefix = `${this.filterToLog()}.fireFilterSync()`;
196
+ _package.logger.debug(`${logPrefix}, ` +
197
+ `bioFilter = ${!!this.bioFilter ? this.bioFilter.constructor.name : 'null'}` +
198
+ (!!this.bioFilter ? `, props = ${JSON.stringify(this.bioFilter!.props.save())}` : ''));
199
+
200
+ grok.events.fireCustomEvent(FILTER_SYNC_EVENT, new FilterState(
201
+ this.bioFilter!.props, this.filterId, this.dataFrame!.id, this.columnName!, this.bitset));
120
202
  }
121
203
 
204
+ // -- Handle events
205
+
122
206
  /**
123
207
  * Performs the actual filtering
124
208
  * When the results are ready, triggers `rows.requestFilter`, which in turn triggers `applyFilter`
125
209
  * that would simply apply the bitset synchronously.
126
210
  */
127
- async _onInputChanged(): Promise<void> {
128
- _package.logger.debug('Bio: BioSubstructureFilter._onInputChanged(), start');
211
+ bioFilterOnChangedDebounced(): void {
212
+ if (!this.dataFrame) return; // Debounced event can be handled postponed
213
+ const logPrefix = `${this.filterToLog()}.bioFilterOnChangedDebounced()`;
214
+ _package.logger.debug(`${logPrefix}, start, ` +
215
+ `isFiltering = ${this.isFiltering}, ` +
216
+ `props = ${JSON.stringify(this.bioFilter!.props.save())}`);
217
+
129
218
  if (!this.isFiltering) {
130
219
  this.bitset = null;
131
- this.dataFrame?.rows.requestFilter();
132
- } else if (wu(this.dataFrame!.rows.filters).has(`${this.columnName}: ${this.filterSummary}`)) {
133
- // some other filter is already filtering for the exact same thing
220
+ this.dataFrame!.rows.requestFilter();
221
+ return;
222
+ }
223
+
224
+ // some other filter is already filtering for the exact same thing
225
+ if (wu(this.dataFrame!.rows.filters).has(`${this.columnName}: ${this.filterSummary}`))
134
226
  return;
135
- } else {
227
+
228
+ this.filterSyncer.sync(logPrefix, async () => {
136
229
  this.calculating = true;
137
230
  try {
231
+ _package.logger.debug(`${logPrefix}, before substructureSearch`);
138
232
  this.bitset = await this.bioFilter?.substructureSearch(this.column!)!;
233
+ _package.logger.debug(`${logPrefix}, after substructureSearch`);
139
234
  this.calculating = false;
235
+ this.fireFilterSync();
140
236
  this.dataFrame?.rows.requestFilter();
141
237
  } finally {
142
238
  this.calculating = false;
239
+ _package.logger.debug(`${logPrefix}, end`);
143
240
  }
144
- }
241
+ });
145
242
  }
146
- }
147
243
 
148
- abstract class BioFilterBase {
149
- onChanged: Subject<any> = new Subject<any>();
244
+ // -- IRenderer --
150
245
 
151
- get filterPanel() {
152
- return new HTMLElement();
153
- }
246
+ private _onRendered = new Subject<void>();
154
247
 
155
- abstract get substructure(): string;
156
- abstract set substructure(s: string);
248
+ get onRendered(): Observable<void> { return this._onRendered; }
157
249
 
158
- async substructureSearch(_column: DG.Column): Promise<DG.BitSet | null> {
159
- return null;
250
+ invalidate(caller?: string): void {
251
+ const logPrefix = `${this.filterToLog()}.invalidate(${caller ? ` <- ${caller} ` : ''})`;
252
+ this.filterSyncer.sync(logPrefix, async () => { this._onRendered.next(); });
160
253
  }
161
254
 
162
- abstract resetFilter(): void;
255
+ async awaitRendered(timeout: number = 10000): Promise<void> {
256
+ const callLog = `awaitRendered( ${timeout} )`;
257
+ const logPrefix = `${this.filterToLog()}.${callLog}`;
258
+ await delay(0);
259
+ await testEvent(this.onRendered, () => {
260
+ this.logger.debug(`${logPrefix}, ` + '_onRendered event caught');
261
+ }, () => {
262
+ this.invalidate(callLog);
263
+ }, timeout, `${logPrefix} ${timeout} timeout`);
163
264
 
164
- abstract detach(): void;
265
+ // Rethrow stored syncer error (for test purposes)
266
+ const viewErrors = this.filterSyncer.resetErrors();
267
+ if (viewErrors.length > 0) throw viewErrors[0];
268
+ }
165
269
  }
166
270
 
167
- class FastaFilter extends BioFilterBase {
271
+ export class FastaBioFilter extends BioFilterBase<BioFilterProps> {
272
+ readonly emptyProps = new BioFilterProps('');
273
+
168
274
  readonly substructureInput: DG.InputBase<string>;
169
275
 
276
+ get type(): string { return 'FastaBioFilter'; }
277
+
170
278
  constructor() {
171
279
  super();
172
280
 
173
281
  this.substructureInput = ui.stringInput('', '', () => {
174
- this.onChanged.next();
282
+ this.props.substructure = this.substructureInput.value;
283
+ if (!this._propsChanging) this.onChanged.next();
175
284
  }, {placeholder: 'Substructure'});
176
285
  }
177
286
 
178
- get filterPanel() {
179
- return this.substructureInput.root;
287
+ public applyProps() {
288
+ this.substructureInput.value = this.props.substructure;
180
289
  }
181
290
 
182
- get substructure() {
183
- return this.substructureInput.value;
291
+ get filterPanel() {
292
+ return this.substructureInput.root;
184
293
  }
185
294
 
186
- set substructure(s: string) {
187
- this.substructureInput.value = s;
188
- }
295
+ get isFiltering(): boolean { return this.substructureInput.value !== ''; }
189
296
 
190
297
  async substructureSearch(column: DG.Column): Promise<DG.BitSet | null> {
191
- return linearSubstructureSearch(this.substructure, column);
298
+ return linearSubstructureSearch(this.props.substructure, column);
192
299
  }
193
300
 
194
- resetFilter(): void {
195
- this.substructureInput.value = '';
196
- }
301
+ async attach(): Promise<void> {}
197
302
 
198
- detach(): void { }
303
+ async detach(): Promise<void> {
304
+ await super.detach();
305
+ }
199
306
  }
200
307
 
201
- export class SeparatorFilter extends FastaFilter {
308
+ export class SeparatorBioFilter extends BioFilterBase<SeparatorFilterProps> {
309
+ readonly emptyProps = new SeparatorFilterProps('');
310
+
311
+ readonly substructureInput: DG.InputBase<string>;
202
312
  readonly separatorInput: DG.InputBase<string>;
203
313
  colSeparator = '';
204
314
 
205
- constructor(separator: string) {
315
+ get type(): string { return 'SeparatorBioFilter'; }
316
+
317
+ constructor(colSeparator: string) {
206
318
  super();
207
319
 
208
- this.separatorInput = ui.stringInput('', '', () => {
209
- this.onChanged.next();
320
+ this.substructureInput = ui.stringInput('', '', () => {
321
+ this.props.substructure = this.substructureInput.value;
322
+ if (!this._propsChanging) this.onChanged.next();
323
+ }, {placeholder: 'Substructure'});
324
+ this.separatorInput = ui.stringInput('', this.colSeparator = colSeparator, () => {
325
+ this.props.separator = !!this.separatorInput.value ? this.separatorInput.value : undefined;
326
+ if (!this._propsChanging) this.onChanged.next();
210
327
  }, {placeholder: 'Separator'});
211
- this.colSeparator = separator;
212
- this.separatorInput.value = separator;
328
+ }
329
+
330
+ applyProps(): void {
331
+ this.substructureInput.value = this.props.substructure;
332
+ this.separatorInput.value = this.props.separator ?? this.colSeparator;
333
+ }
334
+
335
+ get filterSummary(): string {
336
+ const sep: string = this.props.separator ? this.props.separator : this.colSeparator;
337
+ return `${this.props.substructure}, {sep}`;
338
+ };
339
+
340
+ get isFiltering(): boolean { return this.props.substructure !== ''; };
341
+
342
+ resetFilter(): void {
343
+ this.props = new SeparatorFilterProps('');
213
344
  }
214
345
 
215
346
  get filterPanel() {
@@ -233,100 +364,9 @@ export class SeparatorFilter extends FastaFilter {
233
364
  return linearSubstructureSearch(this.substructure, column, this.colSeparator);
234
365
  }
235
366
 
236
- detach(): void { }
237
- }
238
-
239
- export class HelmFilter extends BioFilterBase {
240
- helmEditor: IHelmWebEditor;
241
- _filterPanel = ui.div('', {style: {cursor: 'pointer'}});
242
- helmSubstructure = '';
243
-
244
- constructor() {
245
- super();
246
- this.init();
247
- }
248
-
249
- viewSubs: Unsubscribable[] = [];
250
-
251
- async init() {
252
- this.helmEditor = await grok.functions.call('Helm:helmWebEditor');
253
- await ui.tools.waitForElementInDom(this._filterPanel);
254
- this.updateFilterPanel();
255
- let editorDiv: HTMLDivElement | undefined;
256
- let webEditor: any | undefined;
257
- // TODO: Unsubscribe 'click' and 'sizeChanged'
258
- this.viewSubs.push(fromEvent(this._filterPanel, 'click').subscribe(() => {
259
- ({editorDiv, webEditor} = this.helmEditor.createWebEditor(this.helmSubstructure));
260
- const dlg = ui.dialog({showHeader: false, showFooter: true})
261
- .add(editorDiv)
262
- .onOK(() => {
263
- const helmString = webEditor.canvas.getHelm(true)
264
- .replace(/<\/span>/g, '').replace(/<span style='background:#bbf;'>/g, '');
265
- this.helmSubstructure = helmString;
266
- this.updateFilterPanel(this.substructure);
267
- setTimeout(() => { this.onChanged.next(); }, 10);
268
- }).show({modal: true, fullScreen: true});
269
- const onCloseSub = dlg.onClose.subscribe(() => {
270
- onCloseSub.unsubscribe();
271
- editorDiv = undefined;
272
- webEditor = undefined;
273
- });
274
- }));
275
- this.viewSubs.push(ui.onSizeChanged(this._filterPanel).subscribe((_) => {
276
- try {
277
- if (!!webEditor) {
278
- const helmString = webEditor.canvas.getHelm(true)
279
- .replace(/<\/span>/g, '').replace(/<span style='background:#bbf;'>/g, '');
280
- this.updateFilterPanel(helmString);
281
- }
282
- } catch (err: any) {
283
- const [errMsg, errStack] = errInfo(err);
284
- _package.logger.error(errMsg, undefined, errStack);
285
- }
286
- }));
287
- }
288
-
289
- get filterPanel() {
290
- return this._filterPanel;
291
- }
292
-
293
- get substructure() {
294
- return this.helmSubstructure;
295
- }
296
-
297
- set substructure(s: string) {
298
- this.helmEditor.editor.setHelm(s);
299
- }
300
-
301
- updateFilterPanel(helmString?: string) {
302
- const width = this._filterPanel.parentElement!.clientWidth < 100 ? 100 :
303
- this._filterPanel.parentElement!.clientWidth;
304
- const height = width / 2;
305
- if (!helmString) {
306
- const editDiv = ui.divText('Click to edit', 'helm-substructure-filter');
307
- updateDivInnerHTML(this._filterPanel, editDiv);
308
- } else {
309
- updateDivInnerHTML(this._filterPanel, this.helmEditor.host);
310
- this.helmEditor.editor.setHelm(helmString);
311
- this.helmEditor.resizeEditor(width, height);
312
- }
313
- }
314
-
315
- async substructureSearch(column: DG.Column): Promise<DG.BitSet | null> {
316
- ui.setUpdateIndicator(this._filterPanel, true);
317
- await delay(10);
318
- const res = await helmSubstructureSearch(this.substructure, column);
319
- ui.setUpdateIndicator(this._filterPanel, false);
320
- return res;
321
- }
322
-
323
- resetFilter(): void {
324
- console.debug('Bio: HelmFilter.resetFilter()');
325
- this.helmSubstructure = '';
326
- this.updateFilterPanel(this.substructure);
327
- }
367
+ async attach(): Promise<void> {}
328
368
 
329
- detach(): void {
330
- for (const sub of this.viewSubs) sub.unsubscribe();
369
+ async detach(): Promise<void> {
370
+ await super.detach();
331
371
  }
332
372
  }
@@ -11,7 +11,7 @@ import '../../css/composition-analysis.css';
11
11
  import {UnitsHandler} from '@datagrok-libraries/bio/src/utils/units-handler';
12
12
 
13
13
 
14
- export function getCompositionAnalysisWidget(val: DG.SemanticValue) {
14
+ export function getCompositionAnalysisWidget(val: DG.SemanticValue): DG.Widget {
15
15
  const host = ui.div();
16
16
  host.classList.add('macromolecule-cell-comp-analysis-host');
17
17
  const alphabet = val.cell.column.tags[bioTAGS.alphabet];