@datagrok/peptides 1.16.0 → 1.17.1

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.
Files changed (60) hide show
  1. package/.eslintrc.json +17 -6
  2. package/CHANGELOG.md +33 -8
  3. package/README.md +12 -7
  4. package/dist/196.js +2 -3
  5. package/dist/23.js +2 -0
  6. package/dist/282.js +2 -0
  7. package/dist/361.js +2 -2
  8. package/dist/40.js +2 -0
  9. package/dist/436.js +2 -2
  10. package/dist/65.js +2 -0
  11. package/dist/704.js +2 -0
  12. package/dist/package-test.js +2 -3
  13. package/dist/package.js +2 -3
  14. package/package.json +13 -13
  15. package/setup-unlink-clean.cmd +6 -0
  16. package/setup.cmd +2 -2
  17. package/src/demo/fasta.ts +8 -2
  18. package/src/model.ts +857 -560
  19. package/src/package-test.ts +1 -3
  20. package/src/package.ts +28 -50
  21. package/src/tests/benchmarks.ts +31 -11
  22. package/src/tests/core.ts +11 -6
  23. package/src/tests/misc.ts +6 -6
  24. package/src/tests/model.ts +80 -45
  25. package/src/tests/table-view.ts +49 -39
  26. package/src/tests/utils.ts +0 -76
  27. package/src/tests/viewers.ts +30 -12
  28. package/src/tests/widgets.ts +30 -11
  29. package/src/utils/algorithms.ts +115 -38
  30. package/src/utils/cell-renderer.ts +217 -96
  31. package/src/utils/constants.ts +37 -7
  32. package/src/utils/misc.ts +285 -30
  33. package/src/utils/parallel-mutation-cliffs.ts +18 -15
  34. package/src/utils/statistics.ts +70 -14
  35. package/src/utils/tooltips.ts +46 -25
  36. package/src/utils/types.ts +29 -26
  37. package/src/utils/worker-creator.ts +5 -0
  38. package/src/viewers/logo-summary.ts +597 -135
  39. package/src/viewers/sar-viewer.ts +946 -249
  40. package/src/widgets/distribution.ts +291 -196
  41. package/src/widgets/manual-alignment.ts +18 -11
  42. package/src/widgets/mutation-cliffs.ts +45 -21
  43. package/src/widgets/peptides.ts +86 -91
  44. package/src/widgets/selection.ts +56 -22
  45. package/src/widgets/settings.ts +94 -44
  46. package/src/workers/dimensionality-reducer.ts +5 -6
  47. package/src/workers/mutation-cliffs-worker.ts +3 -16
  48. package/dist/196.js.LICENSE.txt +0 -51
  49. package/dist/209.js +0 -2
  50. package/dist/381.js +0 -2
  51. package/dist/694.js +0 -2
  52. package/dist/831.js +0 -2
  53. package/dist/868.js +0 -2
  54. package/dist/package-test.js.LICENSE.txt +0 -51
  55. package/dist/package.js.LICENSE.txt +0 -51
  56. package/src/tests/peptide-space-test.ts +0 -48
  57. package/src/tests/test-data.ts +0 -649
  58. package/src/utils/molecular-measure.ts +0 -174
  59. package/src/utils/peptide-similarity-space.ts +0 -216
  60. package/src/viewers/peptide-space-viewer.ts +0 -150
@@ -4,297 +4,888 @@ import * as DG from 'datagrok-api/dg';
4
4
 
5
5
  import $ from 'cash-dom';
6
6
  import * as C from '../utils/constants';
7
+ import {COLUMN_NAME} from '../utils/constants';
7
8
  import * as CR from '../utils/cell-renderer';
8
9
  import {PeptidesModel, VIEWER_TYPE} from '../model';
9
10
  import wu from 'wu';
10
- import {SelectionItem} from '../utils/types';
11
- import {PositionStats, Stats} from '../utils/statistics';
11
+ import * as type from '../utils/types';
12
+ import {MutationCliffs, PeptidesSettings, SelectionItem} from '../utils/types';
13
+ import {
14
+ AggregationColumns,
15
+ getAggregatedColName,
16
+ getAggregatedColumnValuesFromDf,
17
+ getAggregatedValue,
18
+ MonomerPositionStats,
19
+ PositionStats,
20
+ StatsItem,
21
+ } from '../utils/statistics';
12
22
  import {_package} from '../package';
13
23
  import {showTooltip} from '../utils/tooltips';
24
+ import {calculateMonomerPositionStatistics, findMutations, MutationCliffsOptions} from '../utils/algorithms';
25
+ import {
26
+ extractColInfo,
27
+ getTotalAggColumns,
28
+ highlightMonomerPosition,
29
+ initSelection,
30
+ isApplicableDataframe,
31
+ isSelectionEmpty,
32
+ modifySelection,
33
+ scaleActivity,
34
+ } from '../utils/misc';
35
+ import {splitAlignedSequences} from '@datagrok-libraries/bio/src/utils/splitter';
36
+ import {LogoSummaryTable} from './logo-summary';
37
+ import {TAGS as bioTAGS} from '@datagrok-libraries/bio/src/utils/macromolecule/consts';
38
+ import {ALPHABET} from '@datagrok-libraries/bio/src/utils/macromolecule';
14
39
 
15
40
  export enum SELECTION_MODE {
16
41
  MUTATION_CLIFFS = 'Mutation Cliffs',
17
42
  INVARIANT_MAP = 'Invariant Map',
18
43
  }
19
44
 
20
- export enum MONOMER_POSITION_PROPERTIES {
21
- COLOR_COLUMN_NAME = 'color',
22
- AGGREGATION = 'aggregation',
45
+ export enum SAR_PROPERTIES {
46
+ SEQUENCE = 'sequence',
47
+ ACTIVITY = 'activity',
48
+ ACTIVITY_SCALING = 'activityScaling',
23
49
  TARGET = 'target',
24
- };
50
+ TARGET_CATEGORY = 'targetCategory',
51
+ MIN_ACTIVITY_DELTA = 'minActivityDelta',
52
+ MAX_MUTATIONS = 'maxMutations',
53
+ COLUMNS = 'columns',
54
+ AGGREGATION = 'aggregation',
55
+ }
25
56
 
26
- /** Structure-activity relationship viewer */
27
- export class MonomerPosition extends DG.JsViewer {
28
- _titleHost = ui.divText(SELECTION_MODE.MUTATION_CLIFFS, {id: 'pep-viewer-title'});
29
- _viewerGrid!: DG.Grid;
30
- _model!: PeptidesModel;
31
- color: string;
32
- aggregation: string;
33
- target: string;
34
- keyPressed: boolean = false;
35
- currentGridCell: DG.GridCell | null = null;
57
+ export enum MONOMER_POSITION_PROPERTIES {
58
+ COLOR = 'color',
59
+ COLOR_AGGREGATION = 'colorAggregation',
60
+ }
36
61
 
37
- constructor() {
62
+ export enum PROPERTY_CATEGORIES {
63
+ GENERAL = 'General',
64
+ INVARIANT_MAP = 'Invariant Map',
65
+ MUTATION_CLIFFS = 'Mutation Cliffs',
66
+ AGGREGATION = 'Aggregation',
67
+ }
68
+
69
+ const MUTATION_CLIFFS_CELL_WIDTH = 40;
70
+ const AAR_CELL_WIDTH = 30;
71
+
72
+ export interface ISARViewer {
73
+ sequenceColumnName: string;
74
+ activityColumnName: string;
75
+ activityScaling: C.SCALING_METHODS;
76
+ minActivityDelta: number;
77
+ maxMutations: number;
78
+ }
79
+
80
+ /** Abstract class for MonomerPosition and MostPotentResidues viewers. */
81
+ export abstract class SARViewer extends DG.JsViewer implements ISARViewer {
82
+ keyPressed: boolean = false;
83
+ sequenceColumnName: string;
84
+ activityColumnName: string;
85
+ activityScaling: C.SCALING_METHODS;
86
+ columns: string[];
87
+ aggregation: string;
88
+ targetColumnName: string;
89
+ targetCategory: string;
90
+ minActivityDelta: number;
91
+ maxMutations: number;
92
+ _scaledActivityColumn: DG.Column | null = null;
93
+ doRender: boolean = true;
94
+
95
+ /** Sets common properties for inheritor viewers. */
96
+ protected constructor() {
38
97
  super();
39
- this.target = this.string(MONOMER_POSITION_PROPERTIES.TARGET, null,
40
- {category: SELECTION_MODE.MUTATION_CLIFFS, choices: []});
41
- this.color = this.string(MONOMER_POSITION_PROPERTIES.COLOR_COLUMN_NAME, C.COLUMNS_NAMES.ACTIVITY_SCALED,
42
- {category: SELECTION_MODE.INVARIANT_MAP,
43
- choices: wu(grok.shell.t.columns.numerical).toArray().map((col) => col.name)});
44
- this.aggregation = this.string(MONOMER_POSITION_PROPERTIES.AGGREGATION, DG.AGG.AVG,
45
- {category: SELECTION_MODE.INVARIANT_MAP,
46
- choices: Object.values(DG.AGG)
47
- .filter((agg) => ![DG.AGG.KEY, DG.AGG.PIVOT, DG.AGG.SELECTED_ROWS_COUNT].includes(agg))});
98
+ // General properties
99
+ this.sequenceColumnName = this.column(SAR_PROPERTIES.SEQUENCE,
100
+ {category: PROPERTY_CATEGORIES.GENERAL, semType: DG.SEMTYPE.MACROMOLECULE, nullable: false});
101
+ this.activityColumnName = this.column(SAR_PROPERTIES.ACTIVITY,
102
+ {category: PROPERTY_CATEGORIES.GENERAL, nullable: false});
103
+ this.activityScaling = this.string(SAR_PROPERTIES.ACTIVITY_SCALING, C.SCALING_METHODS.NONE,
104
+ {category: PROPERTY_CATEGORIES.GENERAL, choices: Object.values(C.SCALING_METHODS), nullable: false},
105
+ ) as C.SCALING_METHODS;
106
+
107
+ // Mutation Cliffs properties
108
+ this.targetColumnName = this.column(SAR_PROPERTIES.TARGET, {category: PROPERTY_CATEGORIES.MUTATION_CLIFFS});
109
+ this.targetCategory = this.string(SAR_PROPERTIES.TARGET_CATEGORY, null,
110
+ {category: PROPERTY_CATEGORIES.MUTATION_CLIFFS, choices: []});
111
+ this.minActivityDelta = this.float(SAR_PROPERTIES.MIN_ACTIVITY_DELTA, 0,
112
+ {category: PROPERTY_CATEGORIES.MUTATION_CLIFFS, min: 0, max: 100});
113
+ this.maxMutations = this.int(SAR_PROPERTIES.MAX_MUTATIONS, 1,
114
+ {category: PROPERTY_CATEGORIES.MUTATION_CLIFFS, min: 1, max: 50});
115
+
116
+ this.columns = this.columnList(SAR_PROPERTIES.COLUMNS, [], {category: PROPERTY_CATEGORIES.AGGREGATION});
117
+ this.aggregation = this.string(SAR_PROPERTIES.AGGREGATION, DG.AGG.AVG,
118
+ {category: PROPERTY_CATEGORIES.AGGREGATION, choices: C.AGGREGATION_TYPES});
48
119
  }
49
120
 
50
- get name(): string {return VIEWER_TYPE.MONOMER_POSITION;}
121
+ _viewerGrid: DG.Grid | null = null;
51
122
 
123
+ /**
124
+ * Returns SARViewer grid. Creates a new one if it is null.
125
+ * @return - SARViewer grid.
126
+ */
52
127
  get viewerGrid(): DG.Grid {
53
- if (!this._viewerGrid)
54
- this.createMonomerPositionGrid();
128
+ this._viewerGrid ??= this.createViewerGrid();
55
129
  return this._viewerGrid;
56
130
  }
57
- set viewerGrid(grid: DG.Grid) {
58
- this._viewerGrid = grid;
131
+
132
+ /**
133
+ * Returns sequence column alphabet.
134
+ * @return - sequence column alphabet.
135
+ */
136
+ get alphabet(): string {
137
+ const col = this.dataFrame.getCol(this.sequenceColumnName);
138
+ return col.getTag(bioTAGS.alphabet) ?? ALPHABET.UN;
139
+ }
140
+
141
+ /**
142
+ * Returns PeptidesModel instance that belongs to the attached dataframe.
143
+ * @return - PeptidesModel instance.
144
+ */
145
+ get model(): PeptidesModel {
146
+ return PeptidesModel.getInstance(this.dataFrame);
147
+ }
148
+
149
+ _positionColumns: DG.Column<string>[] | null = null;
150
+
151
+ /**
152
+ * Returns position columns. If position columns are not attached to the viewer, it tries to get them from
153
+ * other viewers or analysis (given that relevant parameters are the same), or creates own position columns.
154
+ * @return - position columns.
155
+ */
156
+ get positionColumns(): DG.Column<string>[] {
157
+ if (this._positionColumns != null) {
158
+ return this._positionColumns;
159
+ }
160
+
161
+
162
+ const getSharedPositionColumns = (viewerType: VIEWER_TYPE): DG.Column<string>[] | null => {
163
+ const viewer = this.model.findViewer(viewerType) as SARViewer | LogoSummaryTable | null;
164
+ if (this.sequenceColumnName === viewer?.sequenceColumnName) {
165
+ return viewer._positionColumns;
166
+ }
167
+
168
+
169
+ return null;
170
+ };
171
+
172
+ if (this.model.positionColumns != null && this.sequenceColumnName === this.model.settings?.sequenceColumnName) {
173
+ this._positionColumns = this.model.positionColumns;
174
+ } else if (this instanceof MonomerPosition) {
175
+ this._positionColumns = getSharedPositionColumns(VIEWER_TYPE.MOST_POTENT_RESIDUES);
176
+ } else if (this instanceof MostPotentResidues) {
177
+ this._positionColumns = getSharedPositionColumns(VIEWER_TYPE.MONOMER_POSITION);
178
+ }
179
+
180
+
181
+ this._positionColumns ??= getSharedPositionColumns(VIEWER_TYPE.LOGO_SUMMARY_TABLE) ??
182
+ splitAlignedSequences(this.dataFrame.getCol(this.sequenceColumnName)).columns.toList();
183
+ return this._positionColumns!;
184
+ }
185
+
186
+ _monomerPositionStats: MonomerPositionStats | null = null;
187
+
188
+ /**
189
+ * Gets monomer-position statistics. If monomer-position statistics are not attached to the viewer, it tries to get
190
+ * them from other viewers or analysis (given that relevant parameters are the same), or calculates own statistics.
191
+ * @return - monomer-position statistics.
192
+ */
193
+ get monomerPositionStats(): MonomerPositionStats {
194
+ if (this._monomerPositionStats != null) {
195
+ return this._monomerPositionStats;
196
+ }
197
+
198
+
199
+ const isMonomerPositionStatsEqual = (other: SARViewer | PeptidesSettings | null): boolean =>
200
+ this.sequenceColumnName === other?.sequenceColumnName &&
201
+ this.activityColumnName === other?.activityColumnName &&
202
+ this.activityScaling === other?.activityScaling;
203
+
204
+ const getSharedStats = (viewerType: VIEWER_TYPE): MonomerPositionStats | null => {
205
+ const viewer = this.model.findViewer(viewerType) as SARViewer | null;
206
+ if (isMonomerPositionStatsEqual(viewer)) {
207
+ return viewer!._monomerPositionStats;
208
+ }
209
+
210
+
211
+ return null;
212
+ };
213
+
214
+ if (this.model.monomerPositionStats !== null && isMonomerPositionStatsEqual(this.model.settings)) {
215
+ this._monomerPositionStats = this.model.monomerPositionStats;
216
+ } else if (this instanceof MonomerPosition) {
217
+ this._monomerPositionStats = getSharedStats(VIEWER_TYPE.MOST_POTENT_RESIDUES);
218
+ } else if (this instanceof MostPotentResidues) {
219
+ this._monomerPositionStats = getSharedStats(VIEWER_TYPE.MONOMER_POSITION);
220
+ }
221
+
222
+
223
+ this._monomerPositionStats ??= calculateMonomerPositionStatistics(this.getScaledActivityColumn(),
224
+ this.dataFrame.filter, this.positionColumns);
225
+ return this._monomerPositionStats;
59
226
  }
60
227
 
228
+ _mutationCliffs: type.MutationCliffs | null = null;
229
+
230
+ /**
231
+ * Gets mutation cliffs. If mutation cliffs are not attached to the viewer, it tries to get them from other viewers,
232
+ * or calculates its own.
233
+ * @return - mutation cliffs.
234
+ */
235
+ get mutationCliffs(): type.MutationCliffs | null {
236
+ if (this._mutationCliffs != null) {
237
+ return this._mutationCliffs;
238
+ }
239
+
240
+
241
+ const isMutationCliffsEqual = (v1: SARViewer, v2: SARViewer | null): boolean =>
242
+ v1.sequenceColumnName === v2?.sequenceColumnName &&
243
+ v1.activityColumnName === v2.activityColumnName &&
244
+ v1.activityScaling === v2.activityScaling &&
245
+ v1.targetColumnName === v2?.targetColumnName &&
246
+ v1.targetCategory === v2?.targetCategory &&
247
+ v1.minActivityDelta === v2?.minActivityDelta &&
248
+ v1.maxMutations === v2?.maxMutations;
249
+
250
+ const getSharedMutationCliffs = (viewerType: VIEWER_TYPE): type.MutationCliffs | null => {
251
+ const viewer = this.model.findViewer(viewerType) as SARViewer | null;
252
+ if (isMutationCliffsEqual(this, viewer)) {
253
+ return viewer!._mutationCliffs;
254
+ }
255
+
256
+
257
+ return null;
258
+ };
259
+
260
+ if (this instanceof MonomerPosition) {
261
+ this._mutationCliffs = getSharedMutationCliffs(VIEWER_TYPE.MOST_POTENT_RESIDUES);
262
+ } else if (this instanceof MostPotentResidues) {
263
+ this._mutationCliffs = getSharedMutationCliffs(VIEWER_TYPE.MONOMER_POSITION);
264
+ }
265
+
266
+
267
+ return this._mutationCliffs;
268
+ }
269
+
270
+ /**
271
+ * Sets mutation cliffs.
272
+ * @param mc - mutation cliffs to set.
273
+ */
274
+ set mutationCliffs(mc: type.MutationCliffs) {
275
+ this._mutationCliffs = mc;
276
+ this.viewerGrid.invalidate();
277
+ }
278
+
279
+ _mutationCliffsSelection: type.Selection | null = null;
280
+
281
+ /**
282
+ * Gets mutation cliffs selection.
283
+ * @return - mutation cliffs selection.
284
+ */
285
+ get mutationCliffsSelection(): type.Selection {
286
+ const tagSuffix = this instanceof MonomerPosition ? C.SUFFIXES.MP : C.SUFFIXES.MPR;
287
+ const tagSelection = this.dataFrame.getTag(`${tagSuffix}${C.TAGS.MUTATION_CLIFFS_SELECTION}`);
288
+ this._mutationCliffsSelection ??= tagSelection === null ? initSelection(this.positionColumns) :
289
+ JSON.parse(tagSelection);
290
+ return this._mutationCliffsSelection!;
291
+ }
292
+
293
+ /**
294
+ * Sets mutation cliffs selection.
295
+ * @param selection - selection to set.
296
+ */
297
+ set mutationCliffsSelection(selection: type.Selection) {
298
+ this._mutationCliffsSelection = selection;
299
+ const tagSuffix = this instanceof MonomerPosition ? C.SUFFIXES.MP : C.SUFFIXES.MPR;
300
+ this.dataFrame.setTag(`${tagSuffix}${C.TAGS.MUTATION_CLIFFS_SELECTION}`, JSON.stringify(selection));
301
+ this.model.fireBitsetChanged(this instanceof MonomerPosition ? VIEWER_TYPE.MONOMER_POSITION :
302
+ VIEWER_TYPE.MOST_POTENT_RESIDUES);
303
+
304
+ const mpViewer = this.model.findViewer(VIEWER_TYPE.MONOMER_POSITION) as MonomerPosition | null;
305
+ mpViewer?.viewerGrid.invalidate();
306
+ const mprViewer = this.model.findViewer(VIEWER_TYPE.MOST_POTENT_RESIDUES) as MostPotentResidues | null;
307
+ mprViewer?.viewerGrid.invalidate();
308
+
309
+ this.model.analysisView.grid.invalidate();
310
+ }
311
+
312
+ /**
313
+ * Gets scaled activity column.
314
+ * @param isFiltered - flag indicating if only filtered rows should be taken into account.
315
+ * @return - scaled activity column.
316
+ */
317
+ getScaledActivityColumn(isFiltered: boolean = false): DG.Column<number> {
318
+ if (this.model.settings?.activityColumnName === this.activityColumnName &&
319
+ this.model.settings?.activityScaling === this.activityScaling) {
320
+ this._scaledActivityColumn = this.model.getScaledActivityColumn(isFiltered);
321
+ }
322
+
323
+
324
+ this._scaledActivityColumn ??= scaleActivity(this.dataFrame.getCol(this.activityColumnName),
325
+ this.activityScaling);
326
+ if (isFiltered) {
327
+ return DG.DataFrame.fromColumns([this._scaledActivityColumn]).clone(this.dataFrame.filter)
328
+ .getCol(this._scaledActivityColumn.name) as DG.Column<number>;
329
+ }
330
+ return this._scaledActivityColumn as DG.Column<number>;
331
+ }
332
+
333
+ /**
334
+ * Modifies mutation cliffs selection. If shift and ctrl keys are both pressed, it removes mutation cliffs from
335
+ * selection. If only shift key is pressed, it adds mutation cliffs to selection. If only ctrl key is pressed, it
336
+ * changes mutation cliffs presence in selection. If none of the keys is pressed, it sets the mutation cliffs as the
337
+ * only selected one.
338
+ * @param monomerPosition - monomer-position to modify selection with.
339
+ * @param options - selection options.
340
+ * @param notify - flag indicating if bitset changed event should fire.
341
+ */
342
+ modifyMutationCliffsSelection(monomerPosition: type.SelectionItem, options: type.SelectionOptions = {
343
+ shiftPressed: false,
344
+ ctrlPressed: false,
345
+ }, notify: boolean = true): void {
346
+ if (notify) {
347
+ this.mutationCliffsSelection = modifySelection(this.mutationCliffsSelection, monomerPosition, options);
348
+ } else {
349
+ this._mutationCliffsSelection = modifySelection(this.mutationCliffsSelection, monomerPosition, options);
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Processes property changes.
355
+ * @param property - changed property.
356
+ */
357
+ onPropertyChanged(property: DG.Property): void {
358
+ super.onPropertyChanged(property);
359
+ this.doRender = true;
360
+ switch (property.name) {
361
+ case `${SAR_PROPERTIES.SEQUENCE}${COLUMN_NAME}`:
362
+ this._positionColumns = null;
363
+ this._monomerPositionStats = null;
364
+ this._mutationCliffs = null;
365
+ this._mutationCliffsSelection = null;
366
+ this._viewerGrid = null;
367
+ break;
368
+ case `${SAR_PROPERTIES.ACTIVITY}${COLUMN_NAME}`:
369
+ case SAR_PROPERTIES.ACTIVITY_SCALING:
370
+ this._monomerPositionStats = null;
371
+ this._mutationCliffs = null;
372
+ this._mutationCliffsSelection = null;
373
+ this._viewerGrid = null;
374
+ this._scaledActivityColumn = null;
375
+ break;
376
+ case `${SAR_PROPERTIES.TARGET}${COLUMN_NAME}`:
377
+ case SAR_PROPERTIES.TARGET_CATEGORY:
378
+ case SAR_PROPERTIES.MIN_ACTIVITY_DELTA:
379
+ case SAR_PROPERTIES.MAX_MUTATIONS:
380
+ this._mutationCliffs = null;
381
+ this._mutationCliffsSelection = null;
382
+ this.doRender = false;
383
+ break;
384
+ case SAR_PROPERTIES.COLUMNS:
385
+ case SAR_PROPERTIES.AGGREGATION:
386
+ if (this instanceof MostPotentResidues) {
387
+ this._viewerGrid = null;
388
+ }
389
+
390
+
391
+ break;
392
+ }
393
+ if (this.mutationCliffs === null && this.sequenceColumnName && this.activityColumnName) {
394
+ this.calculateMutationCliffs().then((mc) => this.mutationCliffs = mc);
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Gets a map of columns and aggregations.
400
+ * @return - map of columns and aggregations.
401
+ */
402
+ getAggregationColumns(): AggregationColumns {
403
+ return Object.fromEntries(
404
+ this.columns.map((colName) => [colName, this.aggregation] as [string, DG.AGG]));
405
+ }
406
+
407
+ /**
408
+ * Gets total viewer aggregated columns from viewer properties and analysis settings if applicable.
409
+ * @return - total viewer aggregated columns.
410
+ */
411
+ getTotalViewerAggColumns(): [string, DG.AggregationType][] {
412
+ const aggrCols = this.getAggregationColumns();
413
+ return getTotalAggColumns(this.columns, aggrCols, this.model?.settings?.columns);
414
+ }
415
+
416
+ /** Creates viewer grid. */
417
+ createViewerGrid(): DG.Grid {
418
+ throw new Error('Not implemented');
419
+ }
420
+
421
+ /** Removes all the active subscriptions. */
422
+ detach(): void {
423
+ this.subs.forEach((sub) => sub.unsubscribe());
424
+ }
425
+
426
+ /** Processes attached table and sets viewer properties. */
427
+ onTableAttached(): void {
428
+ super.onTableAttached();
429
+ this.helpUrl = 'https://datagrok.ai/help/datagrok/solutions/domains/bio/peptides-sar';
430
+ if (isApplicableDataframe(this.dataFrame)) {
431
+ this.getProperty(`${SAR_PROPERTIES.SEQUENCE}${COLUMN_NAME}`)
432
+ ?.set(this, this.dataFrame.columns.bySemType(DG.SEMTYPE.MACROMOLECULE)!.name);
433
+ this.getProperty(`${SAR_PROPERTIES.ACTIVITY}${COLUMN_NAME}`)
434
+ ?.set(this, wu(this.dataFrame.columns.numerical).next().value.name);
435
+ if (this.mutationCliffs === null && this.sequenceColumnName && this.activityColumnName) {
436
+ this.calculateMutationCliffs().then((mc) => this.mutationCliffs = mc);
437
+ }
438
+ } else {
439
+ const msg = 'PeptidesError: dataframe is missing Macromolecule or numeric columns';
440
+ grok.log.error(msg);
441
+ grok.shell.warning(msg);
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Calculates Mutation Cliffs.
447
+ * @return - mutation cliffs.
448
+ */
449
+ async calculateMutationCliffs(): Promise<MutationCliffs> {
450
+ const scaledActivityCol: DG.Column<number> = this.dataFrame.getCol(this.sequenceColumnName);
451
+ //TODO: set categories ordering the same to share compare indexes instead of strings
452
+ const monomerCols: type.RawColumn[] = this.positionColumns.map(extractColInfo);
453
+ const targetCol = this.targetColumnName ? extractColInfo(this.dataFrame.getCol(this.targetColumnName)) : null;
454
+
455
+ const options: MutationCliffsOptions = {
456
+ maxMutations: this.maxMutations, minActivityDelta: this.minActivityDelta,
457
+ targetCol, currentTarget: this.targetCategory,
458
+ };
459
+ return await findMutations(scaledActivityCol.getRawData(), monomerCols, options);
460
+ }
461
+ }
462
+
463
+ /** Structure-activity relationship viewer */
464
+ export class MonomerPosition extends SARViewer {
465
+ colorColumnName: string;
466
+ colorAggregation: string;
467
+ currentGridCell: DG.GridCell | null = null;
468
+
469
+ /** Sets MonomerPosition properties. */
470
+ constructor() {
471
+ super();
472
+
473
+ const colorChoices = wu(grok.shell.t.columns.numerical).toArray().map((col) => col.name);
474
+ this.colorColumnName = this.column(MONOMER_POSITION_PROPERTIES.COLOR,
475
+ {category: PROPERTY_CATEGORIES.INVARIANT_MAP, choices: colorChoices, nullable: false});
476
+ this.colorAggregation = this.string(MONOMER_POSITION_PROPERTIES.COLOR_AGGREGATION, DG.AGG.AVG,
477
+ {category: PROPERTY_CATEGORIES.INVARIANT_MAP, choices: C.AGGREGATION_TYPES});
478
+ }
479
+
480
+ /**
481
+ * Returns viewer name.
482
+ * @return - viewer name.
483
+ */
484
+ get name(): string {
485
+ return VIEWER_TYPE.MONOMER_POSITION;
486
+ }
487
+
488
+ /**
489
+ * Returns current viewer operation mode.
490
+ * @return - current viewer operation mode.
491
+ */
61
492
  get mode(): SELECTION_MODE {
62
493
  return this.dataFrame.getTag(C.TAGS.MONOMER_POSITION_MODE) as SELECTION_MODE ??
63
494
  SELECTION_MODE.MUTATION_CLIFFS;
64
495
  }
496
+
497
+ /**
498
+ * Sets viewer operation mode.
499
+ * @param mode - viewer operation mode to set.
500
+ */
65
501
  set mode(mode: SELECTION_MODE) {
66
502
  this.dataFrame.setTag(C.TAGS.MONOMER_POSITION_MODE, mode);
67
503
  this.viewerGrid.invalidate();
68
504
  }
69
505
 
70
- get model(): PeptidesModel {
71
- this._model ??= PeptidesModel.getInstance(this.dataFrame);
72
- return this._model;
506
+ _invariantMapSelection: type.Selection | null = null;
507
+
508
+ /**
509
+ * Gets invariant map selection. Initializes it if it is null.
510
+ * @return - invariant map selection.
511
+ */
512
+ get invariantMapSelection(): type.Selection {
513
+ const tagSelection = this.dataFrame.getTag(`${C.SUFFIXES.MP}${C.TAGS.INVARIANT_MAP_SELECTION}`);
514
+ this._invariantMapSelection ??= tagSelection === null ? initSelection(this.positionColumns) :
515
+ JSON.parse(tagSelection);
516
+ return this._invariantMapSelection!;
73
517
  }
74
518
 
75
- detach(): void {this.subs.forEach((sub) => sub.unsubscribe());}
519
+ /**
520
+ * Sets invariant map selection and notifies the model.
521
+ * @param selection - selection to set.
522
+ */
523
+ set invariantMapSelection(selection: type.Selection) {
524
+ this._invariantMapSelection = selection;
525
+ this.dataFrame.setTag(`${C.SUFFIXES.MP}${C.TAGS.INVARIANT_MAP_SELECTION}`, JSON.stringify(selection));
526
+ this.model.fireBitsetChanged(VIEWER_TYPE.MONOMER_POSITION);
527
+ this.model.analysisView.grid.invalidate();
528
+ }
76
529
 
530
+ /** Processes attached table and sets viewer properties. */
77
531
  onTableAttached(): void {
78
532
  super.onTableAttached();
79
- this.helpUrl = 'https://datagrok.ai/help/datagrok/solutions/domains/bio/peptides-sar';
533
+ if (isApplicableDataframe(this.dataFrame)) {
534
+ this.getProperty(`${MONOMER_POSITION_PROPERTIES.COLOR}${COLUMN_NAME}`)
535
+ ?.set(this, this.activityColumnName);
536
+ } else {
537
+ const msg = 'PeptidesError: dataframe is missing Macromolecule or numeric columns';
538
+ grok.log.error(msg);
539
+ grok.shell.warning(msg);
540
+ }
80
541
  this.render();
81
542
  }
82
543
 
544
+ /**
545
+ * Modifies invariant map selection. If shift and ctrl keys are both pressed, it removes invariant map from
546
+ * selection. If only shift key is pressed, it adds invariant map to selection. If only ctrl key is pressed, it
547
+ * changes invariant map presence in selection. If none of the keys is pressed, it sets the invariant map as the
548
+ * only selected one.
549
+ * @param monomerPosition - monomer-position to modify selection with.
550
+ * @param options - selection options.
551
+ * @param notify - flag indicating if bitset changed event should fire.
552
+ */
553
+ modifyInvariantMapSelection(monomerPosition: type.SelectionItem, options: type.SelectionOptions = {
554
+ shiftPressed: false,
555
+ ctrlPressed: false,
556
+ }, notify: boolean = true): void {
557
+ if (notify) {
558
+ this.invariantMapSelection = modifySelection(this.invariantMapSelection, monomerPosition, options);
559
+ } else {
560
+ this._invariantMapSelection = modifySelection(this.invariantMapSelection, monomerPosition, options);
561
+ }
562
+ }
563
+
564
+ /**
565
+ * Processes property changes.
566
+ * @param property - changed property.
567
+ */
83
568
  onPropertyChanged(property: DG.Property): void {
84
569
  super.onPropertyChanged(property);
85
- if (property.name === MONOMER_POSITION_PROPERTIES.TARGET)
86
- this.model.updateMutationCliffs().then(() => this.render(true));
570
+ switch (property.name) {
571
+ case MONOMER_POSITION_PROPERTIES.COLOR:
572
+ case MONOMER_POSITION_PROPERTIES.COLOR_AGGREGATION:
573
+ this.viewerGrid.invalidate();
574
+ break;
575
+ case SAR_PROPERTIES.SEQUENCE:
576
+ this._invariantMapSelection = null;
577
+ break;
578
+ }
87
579
 
88
- this.render(true);
580
+ // this will cause colors to recalculate
581
+ this.model.df.columns.toList().forEach((col) => {
582
+ col.temp[C.TAGS.INVARIANT_MAP_COLOR_CACHE] = null;
583
+ });
584
+ if (this.doRender) {
585
+ this.render();
586
+ }
89
587
  }
90
588
 
589
+ /**
590
+ * Creates monomer-position dataframe to be used in MonomerPosition grid.
591
+ * @return - monomer-position dataframe.
592
+ */
91
593
  createMonomerPositionDf(): DG.DataFrame {
92
594
  const uniqueMonomers = new Set<string>();
93
- const splitSeqCols = this.model.positionColumns.toArray();
595
+ const splitSeqCols = this.positionColumns;
94
596
  for (const col of splitSeqCols) {
95
597
  const colCat = col.categories;
96
598
  for (const cat of colCat) {
97
- if (cat !== '')
599
+ if (cat !== '') {
98
600
  uniqueMonomers.add(cat);
601
+ }
99
602
  }
100
603
  }
101
604
 
102
605
  const monomerCol = DG.Column.fromStrings(C.COLUMNS_NAMES.MONOMER, Array.from(uniqueMonomers));
103
606
  const monomerPositionDf = DG.DataFrame.fromColumns([monomerCol]);
104
607
  monomerPositionDf.name = 'SAR';
105
- for (const col of splitSeqCols)
608
+ for (const col of splitSeqCols) {
106
609
  monomerPositionDf.columns.addNewBool(col.name);
610
+ }
611
+
107
612
 
108
613
  return monomerPositionDf;
109
614
  }
110
615
 
111
-
112
- createMonomerPositionGrid(): void {
616
+ /**
617
+ * Builds MonomerPosition interactive grid.
618
+ * @return - MonomerPosition grid.
619
+ */
620
+ createViewerGrid(): DG.Grid {
113
621
  const monomerPositionDf = this.createMonomerPositionDf();
114
- this.viewerGrid = monomerPositionDf.plot.grid();
115
- this.viewerGrid.sort([C.COLUMNS_NAMES.MONOMER]);
116
- this.viewerGrid.columns.setOrder([C.COLUMNS_NAMES.MONOMER, ...this.model.positionColumns.toArray().map((col) => col.name)]);
622
+ const grid = monomerPositionDf.plot.grid();
623
+ grid.sort([C.COLUMNS_NAMES.MONOMER]);
624
+ const positionColumns = this.positionColumns.map((col) => col.name);
625
+ grid.columns.setOrder([C.COLUMNS_NAMES.MONOMER, ...positionColumns]);
117
626
  const monomerCol = monomerPositionDf.getCol(C.COLUMNS_NAMES.MONOMER);
118
- CR.setMonomerRenderer(monomerCol, this.model.alphabet);
119
- this.viewerGrid.onCellRender.subscribe((args: DG.GridCellRenderArgs) => renderCell(args, this.model,
120
- this.mode === SELECTION_MODE.INVARIANT_MAP, this.dataFrame.getCol(this.color), this.aggregation as DG.AGG));
627
+ CR.setMonomerRenderer(monomerCol, this.alphabet);
628
+ grid.onCellRender.subscribe((args: DG.GridCellRenderArgs) => renderCell(args, this,
629
+ this.mode === SELECTION_MODE.INVARIANT_MAP, this.dataFrame.getCol(this.colorColumnName),
630
+ this.colorAggregation as DG.AGG));
121
631
 
122
- this.viewerGrid.onCellTooltip((gridCell: DG.GridCell, x: number, y: number) => {
632
+ grid.onCellTooltip((gridCell: DG.GridCell, x: number, y: number) => {
123
633
  if (!gridCell.isTableCell) {
124
634
  this.model.unhighlight();
125
635
  return true;
126
636
  }
127
637
  const monomerPosition = this.getMonomerPosition(gridCell);
128
- this.model.highlightMonomerPosition(monomerPosition);
129
- return showTooltip(this.model.df, this.model.settings.columns!, {fromViewer: true,
638
+ highlightMonomerPosition(monomerPosition, this.dataFrame, this.monomerPositionStats);
639
+ this.model.isHighlighting = true;
640
+ const columnEntries = this.getTotalViewerAggColumns();
641
+ return showTooltip(this.model.df, this.getScaledActivityColumn(), columnEntries, {
642
+ fromViewer: true,
130
643
  isMutationCliffs: this.mode === SELECTION_MODE.MUTATION_CLIFFS, monomerPosition, x, y,
131
- mpStats: this.model.monomerPositionStats});
644
+ mpStats: this.monomerPositionStats,
645
+ });
132
646
  });
133
- this.viewerGrid.root.addEventListener('mouseleave', (_ev) => this.model.unhighlight());
134
- DG.debounce(this.viewerGrid.onCurrentCellChanged, 500).subscribe((gridCell: DG.GridCell) => {
647
+ grid.root.addEventListener('mouseleave', (_ev) => this.model.unhighlight());
648
+ DG.debounce(grid.onCurrentCellChanged, 500).subscribe((gridCell: DG.GridCell) => {
135
649
  try {
136
- if (!this.keyPressed)
650
+ if (!this.keyPressed) {
137
651
  return;
652
+ }
653
+
654
+
138
655
  if (this.currentGridCell !== null) {
139
656
  const previousMonomerPosition = this.getMonomerPosition(this.currentGridCell);
140
- if (this.mode === SELECTION_MODE.INVARIANT_MAP)
141
- this.model.modifyInvariantMapSelection(previousMonomerPosition, {shiftPressed: true, ctrlPressed: true}, false);
142
- else if (this.model.mutationCliffs?.get(previousMonomerPosition.monomerOrCluster)?.get(previousMonomerPosition.positionOrClusterType)?.size)
143
- this.model.modifyMutationCliffsSelection(previousMonomerPosition, {shiftPressed: true, ctrlPressed: true}, false);
657
+ if (this.mode === SELECTION_MODE.INVARIANT_MAP) {
658
+ this.modifyInvariantMapSelection(previousMonomerPosition, {
659
+ shiftPressed: true,
660
+ ctrlPressed: true,
661
+ }, false);
662
+ } else {
663
+ const hasMutationCliffs = this.mutationCliffs
664
+ ?.get(previousMonomerPosition.monomerOrCluster)?.get(previousMonomerPosition.positionOrClusterType)?.size;
665
+ if (hasMutationCliffs) {
666
+ this.modifyMutationCliffsSelection(previousMonomerPosition, {
667
+ shiftPressed: true,
668
+ ctrlPressed: true,
669
+ }, false);
670
+ }
671
+ }
144
672
  }
145
673
  const monomerPosition = this.getMonomerPosition(gridCell);
146
- if (this.mode === SELECTION_MODE.INVARIANT_MAP)
147
- this.model.modifyInvariantMapSelection(monomerPosition, {shiftPressed: true, ctrlPressed: false}, true);
148
- else if (this.model.mutationCliffs?.get(monomerPosition.monomerOrCluster)?.get(monomerPosition.positionOrClusterType)?.size)
149
- this.model.modifyMutationCliffsSelection(monomerPosition, {shiftPressed: true, ctrlPressed: false}, true);
674
+ if (this.mode === SELECTION_MODE.INVARIANT_MAP) {
675
+ this.modifyInvariantMapSelection(monomerPosition, {shiftPressed: true, ctrlPressed: false}, true);
676
+ } else {
677
+ const hasMutationCliffs = this.mutationCliffs
678
+ ?.get(monomerPosition.monomerOrCluster)?.get(monomerPosition.positionOrClusterType)?.size;
679
+ if (hasMutationCliffs) {
680
+ this.modifyMutationCliffsSelection(monomerPosition, {shiftPressed: true, ctrlPressed: false},
681
+ true);
682
+ }
683
+ }
150
684
 
151
- this.viewerGrid.invalidate();
685
+ grid.invalidate();
152
686
  } finally {
153
687
  this.keyPressed = false;
154
688
  this.currentGridCell = gridCell;
155
689
  }
156
690
  });
157
- this.viewerGrid.root.addEventListener('keydown', (ev) => {
691
+ grid.root.addEventListener('keydown', (ev) => {
158
692
  this.keyPressed = ev.key.startsWith('Arrow');
159
- if (this.keyPressed)
693
+ if (this.keyPressed) {
160
694
  return;
695
+ }
696
+
697
+
161
698
  if (ev.key === 'Escape' || (ev.code === 'KeyA' && ev.ctrlKey && ev.shiftKey)) {
162
- if (this.mode === SELECTION_MODE.INVARIANT_MAP)
163
- this.model.initInvariantMapSelection({notify: false});
164
- else
165
- this.model.initMutationCliffsSelection({notify: false});
699
+ if (this.mode === SELECTION_MODE.INVARIANT_MAP) {
700
+ this._invariantMapSelection = initSelection(this.positionColumns);
701
+ } else {
702
+ this._mutationCliffsSelection = initSelection(this.positionColumns);
703
+ }
166
704
  } else if (ev.code === 'KeyA' && ev.ctrlKey) {
167
- const positions = Object.keys(this.model.monomerPositionStats).filter((pos) => pos !== 'general');
705
+ const positions = Object.keys(this.monomerPositionStats).filter((pos) => pos !== 'general');
168
706
  for (const position of positions) {
169
- const monomers = Object.keys(this.model.monomerPositionStats[position]!).filter((monomer) => monomer !== 'general');
707
+ const monomers = Object.keys(this.monomerPositionStats[position]!)
708
+ .filter((monomer) => monomer !== 'general');
170
709
  for (const monomer of monomers) {
171
710
  const monomerPosition = {monomerOrCluster: monomer, positionOrClusterType: position};
172
- if (this.mode === SELECTION_MODE.INVARIANT_MAP)
173
- this.model.modifyInvariantMapSelection(monomerPosition, {shiftPressed: true, ctrlPressed: false}, false);
174
- else
175
- this.model.modifyMutationCliffsSelection(monomerPosition, {shiftPressed: true, ctrlPressed: false}, false);
711
+ if (this.mode === SELECTION_MODE.INVARIANT_MAP) {
712
+ this.modifyInvariantMapSelection(monomerPosition, {shiftPressed: true, ctrlPressed: false}, false);
713
+ } else {
714
+ this.modifyMutationCliffsSelection(monomerPosition, {
715
+ shiftPressed: true,
716
+ ctrlPressed: false,
717
+ }, false);
718
+ }
176
719
  }
177
720
  }
721
+ } else {
722
+ return;
178
723
  }
179
- this.model.fireBitsetChanged();
180
- this.viewerGrid.invalidate();
724
+
725
+
726
+ this.model.fireBitsetChanged(VIEWER_TYPE.MONOMER_POSITION);
727
+ grid.invalidate();
181
728
  });
182
- this.viewerGrid.root.addEventListener('click', (ev) => {
183
- const gridCell = this.viewerGrid.hitTest(ev.offsetX, ev.offsetY);
184
- if (!gridCell?.isTableCell || gridCell?.tableColumn?.name === C.COLUMNS_NAMES.MONOMER)
729
+ grid.root.addEventListener('click', (ev) => {
730
+ const gridCell = grid.hitTest(ev.offsetX, ev.offsetY);
731
+ if (!gridCell?.isTableCell || gridCell?.tableColumn?.name === C.COLUMNS_NAMES.MONOMER) {
185
732
  return;
733
+ }
734
+
186
735
 
187
736
  const monomerPosition = this.getMonomerPosition(gridCell);
188
737
  if (this.mode === SELECTION_MODE.INVARIANT_MAP) {
189
- this.model.modifyInvariantMapSelection(monomerPosition, {shiftPressed: ev.shiftKey, ctrlPressed: ev.ctrlKey});
190
- if (this.model.isInvariantMapSelectionEmpty)
738
+ this.modifyInvariantMapSelection(monomerPosition, {shiftPressed: ev.shiftKey, ctrlPressed: ev.ctrlKey});
739
+ if (isSelectionEmpty(this.invariantMapSelection)) {
191
740
  monomerPositionDf.currentRowIdx = -1;
192
- } else if (this.model.mutationCliffs?.get(monomerPosition.monomerOrCluster)?.get(monomerPosition.positionOrClusterType)?.size)
193
- this.model.modifyMutationCliffsSelection(monomerPosition, {shiftPressed: ev.shiftKey, ctrlPressed: ev.ctrlKey});
194
-
195
- this.viewerGrid.invalidate();
741
+ }
742
+ } else {
743
+ const hasMutationCliffs = this.mutationCliffs?.get(monomerPosition.monomerOrCluster)
744
+ ?.get(monomerPosition.positionOrClusterType)?.size;
745
+ if (hasMutationCliffs) {
746
+ this.modifyMutationCliffsSelection(monomerPosition,
747
+ {shiftPressed: ev.shiftKey, ctrlPressed: ev.ctrlKey});
748
+ }
749
+ }
750
+ grid.invalidate();
196
751
 
197
752
  this.showHelp();
198
753
  });
199
754
 
200
- setViewerGridProps(this.viewerGrid, false);
755
+ setViewerGridProps(grid);
756
+
757
+ // Monomer cell renderer overrides width settings. This way I ensure is "initially" set.
758
+ const afterDraw = grid.onAfterDrawContent.subscribe(() => {
759
+ const monomerGCol = grid.col(C.COLUMNS_NAMES.MONOMER)!;
760
+ if (monomerGCol.width === AAR_CELL_WIDTH) {
761
+ afterDraw.unsubscribe();
762
+ return;
763
+ }
764
+ monomerGCol.width = AAR_CELL_WIDTH;
765
+ for (const posCol of positionColumns) {
766
+ const gcCol = grid.col(posCol)!;
767
+ gcCol.width = MUTATION_CLIFFS_CELL_WIDTH;
768
+ }
769
+ });
770
+
771
+ return grid;
201
772
  }
202
773
 
774
+ /** Shows viewer context help. */
203
775
  showHelp(): void {
204
776
  _package.files.readAsText('help/monomer-position.md').then((text) => {
205
777
  grok.shell.windows.help.showHelp(ui.markdown(text));
206
778
  }).catch((e) => grok.log.error(e));
207
779
  }
208
780
 
781
+ /**
782
+ * Gets monomer-position from MonomerPosition grid cell.
783
+ * @param gridCell - MonomerPosition grid cell.
784
+ * @return - monomer-position object.
785
+ */
209
786
  getMonomerPosition(gridCell: DG.GridCell): SelectionItem {
210
- return {monomerOrCluster: gridCell.cell.dataFrame.get(C.COLUMNS_NAMES.MONOMER, gridCell!.tableRowIndex!) as string,
211
- positionOrClusterType: gridCell!.tableColumn!.name};
212
- }
213
-
214
- render(refreshOnly = false): void {
215
- if (!refreshOnly) {
216
- $(this.root).empty();
217
- let switchHost = ui.divText(VIEWER_TYPE.MOST_POTENT_RESIDUES, {id: 'pep-viewer-title'});
218
- if (this.name === VIEWER_TYPE.MONOMER_POSITION) {
219
- const mutationCliffsMode = ui.boolInput(SELECTION_MODE.MUTATION_CLIFFS, this.mode === SELECTION_MODE.MUTATION_CLIFFS);
220
- mutationCliffsMode.root.addEventListener('click', () => {
221
- invariantMapMode.value = false;
222
- mutationCliffsMode.value = true;
223
- this.mode = SELECTION_MODE.MUTATION_CLIFFS;
224
- this.showHelp();
225
- });
226
- mutationCliffsMode.setTooltip('Statistically significant changes in activity');
227
- const invariantMapMode = ui.boolInput(SELECTION_MODE.INVARIANT_MAP, this.mode === SELECTION_MODE.INVARIANT_MAP);
228
- invariantMapMode.root.addEventListener('click', () => {
229
- mutationCliffsMode.value = false;
230
- invariantMapMode.value = true;
231
- this.mode = SELECTION_MODE.INVARIANT_MAP;
232
- this.showHelp();
233
- });
234
- invariantMapMode.setTooltip('Number of sequences having monomer-position');
235
- const setDefaultProperties = (input: DG.InputBase): void => {
236
- $(input.root).find('.ui-input-editor').css('margin', '0px').attr('type', 'radio');
237
- $(input.root).find('.ui-input-description').css('padding', '0px').css('padding-right', '16px');
238
- $(input.captionLabel).addClass('ui-label-right');
239
- };
240
- setDefaultProperties(mutationCliffsMode);
241
- setDefaultProperties(invariantMapMode);
242
-
243
- switchHost = ui.divH([mutationCliffsMode.root, invariantMapMode.root], {id: 'pep-viewer-title'});
244
- $(switchHost).css('width', 'auto').css('align-self', 'center');
787
+ return {
788
+ monomerOrCluster: gridCell.cell.dataFrame.get(C.COLUMNS_NAMES.MONOMER, gridCell!.tableRowIndex!) as string,
789
+ positionOrClusterType: gridCell!.tableColumn!.name,
790
+ };
791
+ }
792
+
793
+ /** Renders the MonomerPosition viewer body. */
794
+ render(): void {
795
+ $(this.root).empty();
796
+ if (!this.activityColumnName || !this.sequenceColumnName) {
797
+ this.root.appendChild(ui.divText('Please, select a sequence and activity columns in the viewer properties'));
798
+ return;
799
+ }
800
+ // Backward compatability with 1.16.0
801
+ const columnProperty = this.getProperty(MONOMER_POSITION_PROPERTIES.COLOR);
802
+ if (columnProperty) {
803
+ columnProperty.choices = wu(grok.shell.t.columns.numerical).toArray().map((col) => col.name);
804
+ if (columnProperty.get(this) === C.COLUMNS_NAMES.ACTIVITY_SCALED) {
805
+ columnProperty.set(this, C.COLUMNS_NAMES.ACTIVITY);
245
806
  }
246
- const viewerRoot = this.viewerGrid.root;
247
- viewerRoot.style.width = 'auto';
248
- const header = ui.divH([switchHost], {style: {alignSelf: 'center', lineHeight: 'normal'}});
249
- this.root.appendChild(ui.divV([header, viewerRoot]));
250
807
  }
808
+
809
+ $(this.root).empty();
810
+ let switchHost = ui.divText(VIEWER_TYPE.MOST_POTENT_RESIDUES, {id: 'pep-viewer-title'});
811
+ if (this.name === VIEWER_TYPE.MONOMER_POSITION) {
812
+ const mutationCliffsMode = ui.boolInput(SELECTION_MODE.MUTATION_CLIFFS,
813
+ this.mode === SELECTION_MODE.MUTATION_CLIFFS);
814
+ mutationCliffsMode.root.addEventListener('click', () => {
815
+ invariantMapMode.value = false;
816
+ mutationCliffsMode.value = true;
817
+ this.mode = SELECTION_MODE.MUTATION_CLIFFS;
818
+ this.showHelp();
819
+ });
820
+ mutationCliffsMode.setTooltip('Statistically significant changes in activity');
821
+ const invariantMapMode = ui.boolInput(SELECTION_MODE.INVARIANT_MAP, this.mode === SELECTION_MODE.INVARIANT_MAP);
822
+ invariantMapMode.root.addEventListener('click', () => {
823
+ mutationCliffsMode.value = false;
824
+ invariantMapMode.value = true;
825
+ this.mode = SELECTION_MODE.INVARIANT_MAP;
826
+ this.showHelp();
827
+ });
828
+ invariantMapMode.setTooltip('Number of sequences having monomer-position');
829
+ const setDefaultProperties = (input: DG.InputBase): void => {
830
+ $(input.root).find('.ui-input-editor').css('margin', '0px').attr('type', 'radio');
831
+ $(input.root).find('.ui-input-description').css('padding', '0px').css('padding-right', '16px');
832
+ $(input.captionLabel).addClass('ui-label-right');
833
+ };
834
+ setDefaultProperties(mutationCliffsMode);
835
+ setDefaultProperties(invariantMapMode);
836
+
837
+ switchHost = ui.divH([mutationCliffsMode.root, invariantMapMode.root], {id: 'pep-viewer-title'});
838
+ $(switchHost).css('width', 'auto').css('align-self', 'center');
839
+ }
840
+ const viewerRoot = this.viewerGrid.root;
841
+ viewerRoot.style.width = 'auto';
842
+ const header = ui.divH([switchHost], {style: {alignSelf: 'center', lineHeight: 'normal'}});
843
+ this.root.appendChild(ui.divV([header, viewerRoot]));
251
844
  this.viewerGrid?.invalidate();
252
845
  }
253
846
  }
254
847
 
255
848
  /** Vertical structure activity relationship viewer */
256
- export class MostPotentResidues extends DG.JsViewer {
257
- _titleHost = ui.divText(VIEWER_TYPE.MOST_POTENT_RESIDUES, {id: 'pep-viewer-title'});
258
- _viewerGrid!: DG.Grid;
259
- _model!: PeptidesModel;
260
- keyPressed: boolean = false;
849
+ export class MostPotentResidues extends SARViewer {
261
850
  currentGridRowIdx: number | null = null;
262
851
 
852
+ /** Sets MostPotentResidues properties. */
263
853
  constructor() {
264
854
  super();
265
855
  }
266
856
 
267
- get name(): string {return VIEWER_TYPE.MOST_POTENT_RESIDUES;}
268
-
269
- get viewerGrid(): DG.Grid {
270
- if (!this._viewerGrid)
271
- this.createMostPotentResiduesGrid();
272
- return this._viewerGrid;
273
- }
274
- set viewerGrid(grid: DG.Grid) {
275
- this._viewerGrid = grid;
276
- }
277
-
278
- get model(): PeptidesModel {
279
- this._model ??= PeptidesModel.getInstance(this.dataFrame);
280
- return this._model;
857
+ /**
858
+ * Returns viewer name.
859
+ * @return - viewer name.
860
+ */
861
+ get name(): string {
862
+ return VIEWER_TYPE.MOST_POTENT_RESIDUES;
281
863
  }
282
864
 
283
- detach(): void {this.subs.forEach((sub) => sub.unsubscribe());}
284
-
865
+ /** Processes attached table and sets viewer properties. */
285
866
  onTableAttached(): void {
286
867
  super.onTableAttached();
287
868
  this.helpUrl = 'https://datagrok.ai/help/datagrok/solutions/domains/bio/peptides-sar';
288
869
  this.render();
289
870
  }
290
871
 
872
+ /**
873
+ * Processes property changes.
874
+ * @param property - changed property.
875
+ */
291
876
  onPropertyChanged(property: DG.Property): void {
292
877
  super.onPropertyChanged(property);
293
- this.render();
878
+ if (this.doRender) {
879
+ this.render();
880
+ }
294
881
  }
295
882
 
883
+ /**
884
+ * Creates most potent residues dataframe to be used in MostPotentResidues grid.
885
+ * @return - most potent residues dataframe.
886
+ */
296
887
  createMostPotentResiduesDf(): DG.DataFrame {
297
- const monomerPositionStatsEntries = Object.entries(this.model.monomerPositionStats) as [string, PositionStats][];
888
+ const monomerPositionStatsEntries = Object.entries(this.monomerPositionStats) as [string, PositionStats][];
298
889
  const posData: number[] = new Array(monomerPositionStatsEntries.length - 1);
299
890
  const monomerData: string[] = new Array(posData.length);
300
891
  const mdData: number[] = new Array(posData.length);
@@ -302,37 +893,56 @@ export class MostPotentResidues extends DG.JsViewer {
302
893
  const countData: number[] = new Array(posData.length);
303
894
  const ratioData: number[] = new Array(posData.length);
304
895
  const meanData: number[] = new Array(posData.length);
305
-
896
+ const aggrColumnEntries = this.getTotalViewerAggColumns();
897
+ const aggColNames = aggrColumnEntries.map(([colName, aggFn]) => getAggregatedColName(aggFn, colName));
898
+ const aggrColsData = new Array<Array<number>>(aggColNames.length);
899
+ aggColNames.forEach(((_, idx) => aggrColsData[idx] = new Array<number>(posData.length)));
306
900
  let i = 0;
307
901
  for (const [position, positionStats] of monomerPositionStatsEntries) {
308
902
  const generalPositionStats = positionStats.general;
309
- if (!generalPositionStats)
903
+ if (!generalPositionStats) {
310
904
  continue;
311
- if (Object.entries(positionStats).length === 1)
905
+ }
906
+
907
+
908
+ if (Object.entries(positionStats).length === 1) {
312
909
  continue;
910
+ }
911
+
313
912
 
314
- const filteredMonomerStats: [string, Stats][] = [];
913
+ const filteredMonomerStats: [string, StatsItem][] = [];
315
914
  for (const [monomer, monomerStats] of Object.entries(positionStats)) {
316
- if (monomer === 'general')
915
+ if (monomer === 'general') {
317
916
  continue;
318
- if ((monomerStats as Stats).count > 1 && (monomerStats as Stats).pValue === null)
319
- filteredMonomerStats.push([monomer, monomerStats as Stats]);
917
+ }
918
+
919
+
920
+ if ((monomerStats as StatsItem).count > 1 && (monomerStats as StatsItem).pValue === null) {
921
+ filteredMonomerStats.push([monomer, monomerStats as StatsItem]);
922
+ }
320
923
 
321
- if ((monomerStats as Stats).pValue === generalPositionStats.minPValue)
322
- filteredMonomerStats.push([monomer, monomerStats as Stats]);
924
+
925
+ if ((monomerStats as StatsItem).pValue === generalPositionStats.minPValue) {
926
+ filteredMonomerStats.push([monomer, monomerStats as StatsItem]);
927
+ }
323
928
  }
324
929
 
325
- if (filteredMonomerStats.length === 0)
930
+ if (filteredMonomerStats.length === 0) {
326
931
  continue;
932
+ }
327
933
 
328
- let maxEntry: [string, Stats] | null = null;
934
+
935
+ let maxEntry: [string, StatsItem] | null = null;
329
936
  for (const [monomer, monomerStats] of filteredMonomerStats) {
330
- if (maxEntry === null || maxEntry[1].meanDifference < monomerStats.meanDifference)
937
+ if (maxEntry === null || maxEntry[1].meanDifference < monomerStats.meanDifference) {
331
938
  maxEntry = [monomer, monomerStats];
939
+ }
332
940
  }
333
941
 
334
- if (maxEntry === null)
942
+ if (maxEntry === null) {
335
943
  continue;
944
+ }
945
+
336
946
 
337
947
  posData[i] = parseInt(position);
338
948
  monomerData[i] = maxEntry![0];
@@ -341,6 +951,13 @@ export class MostPotentResidues extends DG.JsViewer {
341
951
  countData[i] = maxEntry![1].count;
342
952
  ratioData[i] = maxEntry![1].ratio;
343
953
  meanData[i] = maxEntry![1].mean;
954
+
955
+ const stats = this.monomerPositionStats[position][maxEntry![0]];
956
+ const mask = DG.BitSet.fromBytes(stats.mask.buffer.buffer, this.model.df.col(this.activityColumnName)!.length);
957
+ for (let j = 0; j < aggColNames.length; j++) {
958
+ const [colName, aggFn] = aggrColumnEntries[j];
959
+ aggrColsData[j][i] = getAggregatedValue(this.model.df.getCol(colName), aggFn, mask);
960
+ }
344
961
  ++i;
345
962
  }
346
963
 
@@ -350,8 +967,9 @@ export class MostPotentResidues extends DG.JsViewer {
350
967
  pValData.length = i;
351
968
  countData.length = i;
352
969
  ratioData.length = i;
970
+ meanData.length = i;
353
971
 
354
- const mprDf = DG.DataFrame.create(i); // Subtract 'general' entry from mp-stats
972
+ const mprDf = DG.DataFrame.create(i);
355
973
  const mprDfCols = mprDf.columns;
356
974
  mprDfCols.add(DG.Column.fromList(DG.TYPE.INT, C.COLUMNS_NAMES.POSITION, posData));
357
975
  mprDfCols.add(DG.Column.fromList(DG.TYPE.STRING, C.COLUMNS_NAMES.MONOMER, monomerData));
@@ -360,112 +978,184 @@ export class MostPotentResidues extends DG.JsViewer {
360
978
  mprDfCols.add(DG.Column.fromList(DG.TYPE.FLOAT, C.COLUMNS_NAMES.P_VALUE, pValData));
361
979
  mprDfCols.add(DG.Column.fromList(DG.TYPE.INT, C.COLUMNS_NAMES.COUNT, countData));
362
980
  mprDfCols.add(DG.Column.fromList(DG.TYPE.FLOAT, C.COLUMNS_NAMES.RATIO, ratioData));
981
+ aggColNames.forEach((it, idx) => mprDfCols.add(DG.Column.fromList(DG.TYPE.FLOAT, it, aggrColsData[idx])));
363
982
 
364
983
  return mprDf;
365
984
  }
366
985
 
367
- createMostPotentResiduesGrid(): void {
368
- const mostPotentResiduesDf = this.createMostPotentResiduesDf();
369
- this.viewerGrid = mostPotentResiduesDf.plot.grid();
370
- this.viewerGrid.sort([C.COLUMNS_NAMES.POSITION]);
371
- const pValGridCol = this.viewerGrid.col(C.COLUMNS_NAMES.P_VALUE)!;
986
+ /**
987
+ * Builds MostPotentResidues interactive grid.
988
+ * @return - MostPotentResidues grid.
989
+ */
990
+ createViewerGrid(): DG.Grid {
991
+ const mprDf = this.createMostPotentResiduesDf();
992
+ const grid = mprDf.plot.grid();
993
+ grid.sort([C.COLUMNS_NAMES.POSITION]);
994
+ const pValGridCol = grid.col(C.COLUMNS_NAMES.P_VALUE)!;
372
995
  pValGridCol.format = '#.000';
373
996
  pValGridCol.name = 'P-value';
374
- const monomerCol = mostPotentResiduesDf.getCol(C.COLUMNS_NAMES.MONOMER);
997
+ const monomerCol = mprDf.getCol(C.COLUMNS_NAMES.MONOMER);
375
998
 
376
999
  // Setting Monomer column renderer
377
- CR.setMonomerRenderer(monomerCol, this.model.alphabet);
378
- this.viewerGrid.onCellRender.subscribe(
379
- (args: DG.GridCellRenderArgs) => renderCell(args, this.model, false, undefined, undefined));
1000
+ CR.setMonomerRenderer(monomerCol, this.alphabet);
1001
+ grid.onCellRender.subscribe(
1002
+ (args: DG.GridCellRenderArgs) => renderCell(args, this, false, undefined, undefined));
380
1003
 
381
- this.viewerGrid.onCellTooltip((gridCell: DG.GridCell, x: number, y: number) => {
1004
+ grid.onCellTooltip((gridCell: DG.GridCell, x: number, y: number) => {
382
1005
  if (!gridCell.isTableCell) {
383
1006
  this.model.unhighlight();
384
1007
  return true;
385
1008
  }
386
1009
  const monomerPosition = this.getMonomerPosition(gridCell);
387
- this.model.highlightMonomerPosition(monomerPosition);
388
- if (gridCell.tableColumn?.name === C.COLUMNS_NAMES.MONOMER)
1010
+ highlightMonomerPosition(monomerPosition, this.dataFrame, this.monomerPositionStats);
1011
+ this.model.isHighlighting = true;
1012
+
1013
+ if (gridCell.tableColumn?.name === C.COLUMNS_NAMES.MONOMER) {
389
1014
  monomerPosition.positionOrClusterType = C.COLUMNS_NAMES.MONOMER;
390
- else if (gridCell.tableColumn?.name !== C.COLUMNS_NAMES.MEAN_DIFFERENCE)
1015
+ } else if (gridCell.tableColumn?.name !== C.COLUMNS_NAMES.MEAN_DIFFERENCE) {
391
1016
  return false;
392
- return showTooltip(this.model.df, this.model.settings.columns!,
393
- {fromViewer: true, isMutationCliffs: true, monomerPosition, x, y, mpStats: this.model.monomerPositionStats});
1017
+ }
1018
+
1019
+
1020
+ const columnEntries = this.getTotalViewerAggColumns();
1021
+ const aggrValues = gridCell.tableRowIndex == null ? undefined :
1022
+ getAggregatedColumnValuesFromDf(gridCell.grid.dataFrame!, gridCell.tableRowIndex, columnEntries, {});
1023
+ return showTooltip(this.model.df, this.getScaledActivityColumn(), columnEntries,
1024
+ {
1025
+ fromViewer: true, isMutationCliffs: true, monomerPosition, x, y, mpStats: this.monomerPositionStats,
1026
+ aggrColValues: aggrValues,
1027
+ });
394
1028
  });
395
- DG.debounce(this.viewerGrid.onCurrentCellChanged, 500).subscribe((gridCell: DG.GridCell) => {
1029
+ DG.debounce(grid.onCurrentCellChanged, 500).subscribe((gridCell: DG.GridCell) => {
396
1030
  try {
397
- if ((this.keyPressed && mostPotentResiduesDf.currentCol.name !== C.COLUMNS_NAMES.MEAN_DIFFERENCE) || !this.keyPressed)
1031
+ if ((this.keyPressed && mprDf.currentCol.name !== C.COLUMNS_NAMES.MEAN_DIFFERENCE) || !this.keyPressed) {
398
1032
  return;
1033
+ }
1034
+
1035
+
399
1036
  const monomerPosition = this.getMonomerPosition(gridCell);
400
1037
  if (this.currentGridRowIdx !== null) {
401
- const previousMonomerPosition = this.getMonomerPosition(this.viewerGrid.cell('Diff', this.currentGridRowIdx));
402
- this.model.modifyMutationCliffsSelection(previousMonomerPosition, {shiftPressed: true, ctrlPressed: true}, false);
1038
+ const previousMonomerPosition = this.getMonomerPosition(grid.cell('Diff', this.currentGridRowIdx));
1039
+ this.modifyMutationCliffsSelection(previousMonomerPosition, {
1040
+ shiftPressed: true,
1041
+ ctrlPressed: true,
1042
+ }, false);
1043
+ }
1044
+ const hasMutationCliffs = this.mutationCliffs?.get(monomerPosition.monomerOrCluster)
1045
+ ?.get(monomerPosition.positionOrClusterType)?.size;
1046
+ if (hasMutationCliffs) {
1047
+ this.modifyMutationCliffsSelection(monomerPosition, {shiftPressed: true, ctrlPressed: false});
403
1048
  }
404
- if (this.model.mutationCliffs?.get(monomerPosition.monomerOrCluster)?.get(monomerPosition.positionOrClusterType)?.size)
405
- this.model.modifyMutationCliffsSelection(monomerPosition, {shiftPressed: true, ctrlPressed: false});
406
- this.viewerGrid.invalidate();
1049
+
1050
+
1051
+ grid.invalidate();
407
1052
  } finally {
408
1053
  this.keyPressed = false;
409
1054
  this.currentGridRowIdx = gridCell.gridRow;
410
1055
  }
411
1056
  });
412
- this.viewerGrid.root.addEventListener('keydown', (ev) => {
1057
+ grid.root.addEventListener('keydown', (ev) => {
413
1058
  this.keyPressed = ev.key.startsWith('Arrow');
414
- if (this.keyPressed)
1059
+ if (this.keyPressed) {
415
1060
  return;
416
- if (ev.key === 'Escape' || (ev.code === 'KeyA' && ev.ctrlKey && ev.shiftKey))
417
- this.model.initMutationCliffsSelection({notify: false});
418
- else if (ev.code === 'KeyA' && ev.ctrlKey) {
419
- for (let rowIdx = 0; rowIdx < mostPotentResiduesDf.rowCount; ++rowIdx) {
420
- const monomerPosition = this.getMonomerPosition(this.viewerGrid.cell('Diff', rowIdx));
421
- this.model.modifyMutationCliffsSelection(monomerPosition, {shiftPressed: true, ctrlPressed: false}, false);
1061
+ }
1062
+
1063
+
1064
+ if (ev.key === 'Escape' || (ev.code === 'KeyA' && ev.ctrlKey && ev.shiftKey)) {
1065
+ this._mutationCliffsSelection = initSelection(this.positionColumns);
1066
+ } else if (ev.code === 'KeyA' && ev.ctrlKey) {
1067
+ for (let rowIdx = 0; rowIdx < mprDf.rowCount; ++rowIdx) {
1068
+ const monomerPosition = this.getMonomerPosition(grid.cell('Diff', rowIdx));
1069
+ this.modifyMutationCliffsSelection(monomerPosition, {shiftPressed: true, ctrlPressed: false}, false);
422
1070
  }
1071
+ } else {
1072
+ return;
423
1073
  }
424
- this.model.fireBitsetChanged();
425
- this.viewerGrid.invalidate();
1074
+
1075
+
1076
+ this.model.fireBitsetChanged(VIEWER_TYPE.MOST_POTENT_RESIDUES);
1077
+ grid.invalidate();
426
1078
  });
427
- this.viewerGrid.root.addEventListener('mouseleave', (_ev) => this.model.unhighlight());
428
- this.viewerGrid.root.addEventListener('click', (ev) => {
429
- const gridCell = this.viewerGrid.hitTest(ev.offsetX, ev.offsetY);
430
- if (!gridCell?.isTableCell || gridCell!.tableColumn!.name !== C.COLUMNS_NAMES.MEAN_DIFFERENCE)
1079
+ grid.root.addEventListener('mouseleave', (_ev) => this.model.unhighlight());
1080
+ grid.root.addEventListener('click', (ev) => {
1081
+ const gridCell = grid.hitTest(ev.offsetX, ev.offsetY);
1082
+ if (!gridCell?.isTableCell || gridCell!.tableColumn!.name !== C.COLUMNS_NAMES.MEAN_DIFFERENCE) {
431
1083
  return;
1084
+ }
1085
+
432
1086
 
433
1087
  const monomerPosition = this.getMonomerPosition(gridCell);
434
- if (!this.model.mutationCliffs?.get(monomerPosition.monomerOrCluster)?.get(monomerPosition.positionOrClusterType)?.size)
1088
+ const hasMutationCliffs = this.mutationCliffs?.get(monomerPosition.monomerOrCluster)
1089
+ ?.get(monomerPosition.positionOrClusterType)?.size;
1090
+ if (!hasMutationCliffs) {
435
1091
  return;
436
- this.model.modifyMutationCliffsSelection(monomerPosition, {shiftPressed: ev.shiftKey, ctrlPressed: ev.ctrlKey});
437
- this.viewerGrid.invalidate();
1092
+ }
1093
+
1094
+
1095
+ this.modifyMutationCliffsSelection(monomerPosition, {shiftPressed: ev.shiftKey, ctrlPressed: ev.ctrlKey});
1096
+ grid.invalidate();
438
1097
 
439
1098
  _package.files.readAsText('help/most-potent-residues.md').then((text) => {
440
1099
  grok.shell.windows.help.showHelp(ui.markdown(text));
441
1100
  }).catch((e) => grok.log.error(e));
442
1101
  });
443
- const mdCol: DG.GridColumn = this.viewerGrid.col(C.COLUMNS_NAMES.MEAN_DIFFERENCE)!;
1102
+
1103
+ setViewerGridProps(grid);
1104
+ const mdCol: DG.GridColumn = grid.col(C.COLUMNS_NAMES.MEAN_DIFFERENCE)!;
444
1105
  mdCol.name = 'Diff';
445
- setViewerGridProps(this.viewerGrid, true);
1106
+ // Monomer cell renderer overrides width settings. This way I ensure is "initially" set.
1107
+ const afterDraw = grid.onAfterDrawContent.subscribe(() => {
1108
+ const monomerGCol = grid.col(C.COLUMNS_NAMES.MONOMER)!;
1109
+ if (monomerGCol.width === AAR_CELL_WIDTH) {
1110
+ afterDraw.unsubscribe();
1111
+ return;
1112
+ }
1113
+ monomerGCol.width = AAR_CELL_WIDTH;
1114
+ mdCol.width = MUTATION_CLIFFS_CELL_WIDTH;
1115
+ });
1116
+
1117
+ return grid;
446
1118
  }
447
1119
 
1120
+ /**
1121
+ * Gets monomer-position from MostPotentResidues grid cell.
1122
+ * @param gridCell - MostPotentResidues grid cell.
1123
+ * @return - monomer-position object.
1124
+ */
448
1125
  getMonomerPosition(gridCell: DG.GridCell): SelectionItem {
449
- return {monomerOrCluster: gridCell.cell.dataFrame.get(C.COLUMNS_NAMES.MONOMER, gridCell!.tableRowIndex!),
450
- positionOrClusterType: `${gridCell.cell.dataFrame.get(C.COLUMNS_NAMES.POSITION, gridCell!.tableRowIndex!)}`};
451
- }
452
-
453
- render(refreshOnly = false): void {
454
- if (!refreshOnly) {
455
- $(this.root).empty();
456
- const switchHost = ui.divText(VIEWER_TYPE.MOST_POTENT_RESIDUES, {id: 'pep-viewer-title'});
457
- const viewerRoot = this.viewerGrid.root;
458
- viewerRoot.style.width = 'auto';
459
- const header = ui.divH([switchHost], {style: {alignSelf: 'center', lineHeight: 'normal'}});
460
- this.root.appendChild(ui.divV([header, viewerRoot]));
1126
+ return {
1127
+ monomerOrCluster: gridCell.cell.dataFrame.get(C.COLUMNS_NAMES.MONOMER, gridCell!.tableRowIndex!),
1128
+ positionOrClusterType: `${gridCell.cell.dataFrame.get(C.COLUMNS_NAMES.POSITION, gridCell!.tableRowIndex!)}`,
1129
+ };
1130
+ }
1131
+
1132
+ /** Renders the MostPotentResidues viewer body. */
1133
+ render(): void {
1134
+ $(this.root).empty();
1135
+ if (!this.activityColumnName || !this.sequenceColumnName) {
1136
+ this.root.appendChild(ui.divText('Please, select a sequence and activity columns in the viewer properties'));
1137
+ return;
461
1138
  }
1139
+ const switchHost = ui.divText(VIEWER_TYPE.MOST_POTENT_RESIDUES, {id: 'pep-viewer-title'});
1140
+ const viewerRoot = this.viewerGrid.root;
1141
+ viewerRoot.style.width = 'auto';
1142
+ const header = ui.divH([switchHost], {style: {alignSelf: 'center', lineHeight: 'normal'}});
1143
+ this.root.appendChild(ui.divV([header, viewerRoot]));
462
1144
  this.viewerGrid?.invalidate();
463
1145
  }
464
1146
  }
465
1147
 
466
- function renderCell(args: DG.GridCellRenderArgs, model: PeptidesModel, isInvariantMap?: boolean,
467
- colorCol?: DG.Column<number>, colorAgg?: DG.AGG, renderNums?: boolean): void {
468
- const renderColNames = [...model.positionColumns.toArray().map((col) => col.name), C.COLUMNS_NAMES.MEAN_DIFFERENCE];
1148
+ /**
1149
+ * Renders SARViewer grid cells.
1150
+ * @param args - grid cell render arguments.
1151
+ * @param viewer - viewer instance.
1152
+ * @param isInvariantMap - flag indicating if viewr is operating in invariant map mode.
1153
+ * @param colorCol - column to use for color coding in invariant map mode.
1154
+ * @param colorAgg - aggregation function to use for color coding in invariant map mode.
1155
+ */
1156
+ function renderCell(args: DG.GridCellRenderArgs, viewer: SARViewer, isInvariantMap?: boolean,
1157
+ colorCol?: DG.Column<number>, colorAgg?: DG.AGG): void {
1158
+ const renderColNames = [...viewer.positionColumns.map((col) => col.name), C.COLUMNS_NAMES.MEAN_DIFFERENCE];
469
1159
  const canvasContext = args.g;
470
1160
  const bound = args.bounds;
471
1161
 
@@ -494,7 +1184,7 @@ function renderCell(args: DG.GridCellRenderArgs, model: PeptidesModel, isInvaria
494
1184
  const currentMonomer: string = gridTable.get(C.COLUMNS_NAMES.MONOMER, tableRowIndex);
495
1185
  const currentPosition: string = tableColName !== C.COLUMNS_NAMES.MEAN_DIFFERENCE ? tableColName :
496
1186
  gridTable.get(C.COLUMNS_NAMES.POSITION, tableRowIndex).toFixed();
497
- const currentPosStats = model.monomerPositionStats[currentPosition];
1187
+ const currentPosStats = viewer.monomerPositionStats[currentPosition];
498
1188
 
499
1189
  if (!currentPosStats![currentMonomer]) {
500
1190
  args.preventDefault();
@@ -504,32 +1194,46 @@ function renderCell(args: DG.GridCellRenderArgs, model: PeptidesModel, isInvaria
504
1194
 
505
1195
  if (isInvariantMap) {
506
1196
  const value = currentPosStats![currentMonomer]!.count;
507
- const positionCol = model.df.getCol(currentPosition);
508
- const positionColData = positionCol.getRawData();
509
- const positionColCategories = positionCol.categories;
510
-
511
- const colorColData = colorCol!.getRawData();
512
- const colorValuesIndexes: number[] = [];
513
- for (let i = 0; i < positionCol.length; ++i) {
514
- if (positionColCategories[positionColData[i]] === currentMonomer)
515
- colorValuesIndexes.push(i);
1197
+ const positionCol = viewer.positionColumns.find((col) => col.name === currentPosition)!;
1198
+ const colorCache: { [_: string]: number } = positionCol.temp[C.TAGS.INVARIANT_MAP_COLOR_CACHE] ?? {};
1199
+ let color: number;
1200
+ if (colorCache[currentMonomer]) {
1201
+ color = colorCache[currentMonomer];
1202
+ } else {
1203
+ const positionColData = positionCol.getRawData();
1204
+ const positionColCategories = positionCol.categories;
1205
+
1206
+ const colorColData = colorCol!.getRawData();
1207
+ const colorValuesIndexes: number[] = [];
1208
+ for (let i = 0; i < positionCol.length; ++i) {
1209
+ if (positionColCategories[positionColData[i]] === currentMonomer) {
1210
+ colorValuesIndexes.push(i);
1211
+ }
1212
+ }
1213
+ const cellColorDataCol = DG.Column.float('color', colorValuesIndexes.length)
1214
+ .init((i) => colorColData[colorValuesIndexes[i]]);
1215
+ const colorColStats = colorCol!.stats;
1216
+ color = DG.Color.scaleColor(cellColorDataCol.aggregate(colorAgg!), colorColStats.min, colorColStats.max);
1217
+ colorCache[currentMonomer] = color;
1218
+ positionCol.temp[C.TAGS.INVARIANT_MAP_COLOR_CACHE] = colorCache;
516
1219
  }
517
- const cellColorDataCol = DG.Column.float('color', colorValuesIndexes.length)
518
- .init((i) => colorColData[colorValuesIndexes[i]]);
519
- const colorColStats = colorCol!.stats;
520
1220
 
521
- const color = DG.Color.scaleColor(cellColorDataCol.aggregate(colorAgg!), colorColStats.min, colorColStats.max);
522
- CR.renderInvaraintMapCell(
523
- canvasContext, currentMonomer, currentPosition, model.invariantMapSelection, value, bound, color);
1221
+ CR.renderInvariantMapCell(canvasContext, currentMonomer, currentPosition,
1222
+ (viewer as MonomerPosition).invariantMapSelection, value, bound, color);
524
1223
  } else {
525
- CR.renderMutationCliffCell(canvasContext, currentMonomer, currentPosition, model.monomerPositionStats, bound,
526
- model.mutationCliffsSelection, model.mutationCliffs, renderNums);
1224
+ CR.renderMutationCliffCell(canvasContext, currentMonomer, currentPosition, viewer, bound);
527
1225
  }
1226
+
1227
+
528
1228
  args.preventDefault();
529
1229
  canvasContext.restore();
530
1230
  }
531
1231
 
532
- function setViewerGridProps(grid: DG.Grid, isMostPotentResiduesGrid: boolean): void {
1232
+ /**
1233
+ * Sets common grid properties.
1234
+ * @param grid - viewer grid.
1235
+ */
1236
+ function setViewerGridProps(grid: DG.Grid): void {
533
1237
  const gridProps = grid.props;
534
1238
  gridProps.allowEdit = false;
535
1239
  gridProps.allowRowSelection = false;
@@ -537,14 +1241,7 @@ function setViewerGridProps(grid: DG.Grid, isMostPotentResiduesGrid: boolean): v
537
1241
  gridProps.allowColSelection = false;
538
1242
  gridProps.showRowHeader = false;
539
1243
  gridProps.showCurrentRowIndicator = false;
1244
+ gridProps.showReadOnlyNotifications = false;
540
1245
 
541
1246
  gridProps.rowHeight = 20;
542
- const girdCols = grid.columns;
543
- const colNum = girdCols.length;
544
- for (let i = 0; i < colNum; ++i) {
545
- const col = girdCols.byIndex(i)!;
546
- const colName = col.name;
547
- col.width = isMostPotentResiduesGrid && colName !== 'Diff' && colName !== C.COLUMNS_NAMES.MONOMER ? 50 :
548
- gridProps.rowHeight + 10;
549
- }
550
1247
  }