@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
package/src/model.ts CHANGED
@@ -1,311 +1,188 @@
1
1
  import * as ui from 'datagrok-api/ui';
2
2
  import * as grok from 'datagrok-api/grok';
3
3
  import * as DG from 'datagrok-api/dg';
4
-
5
- import {splitAlignedSequences} from '@datagrok-libraries/bio/src/utils/splitter';
6
- import {SeqPalette} from '@datagrok-libraries/bio/src/seq-palettes';
7
- import {pickUpPalette, TAGS as bioTAGS, monomerToShort} from '@datagrok-libraries/bio/src/utils/macromolecule';
4
+ import {
5
+ monomerToShort,
6
+ NOTATION,
7
+ pickUpPalette,
8
+ TAGS as bioTAGS,
9
+ } from '@datagrok-libraries/bio/src/utils/macromolecule';
8
10
  import {calculateScores, SCORE} from '@datagrok-libraries/bio/src/utils/macromolecule/scoring';
9
11
  import {Options} from '@datagrok-libraries/utils/src/type-declarations';
10
12
  import {DistanceMatrix} from '@datagrok-libraries/ml/src/distance-matrix';
11
- import {StringMetricsNames} from '@datagrok-libraries/ml/src/typed-metrics';
13
+ import {BitArrayMetrics, StringMetricsNames} from '@datagrok-libraries/ml/src/typed-metrics';
12
14
  import {ITreeHelper} from '@datagrok-libraries/bio/src/trees/tree-helper';
13
15
  import {TAGS as treeTAGS} from '@datagrok-libraries/bio/src/trees';
14
16
  import BitArray from '@datagrok-libraries/utils/src/bit-array';
15
-
17
+ import {UnitsHandler} from '@datagrok-libraries/bio/src/utils/units-handler';
16
18
  import wu from 'wu';
17
19
  import * as rxjs from 'rxjs';
18
- import * as uuid from 'uuid';
19
20
  import $ from 'cash-dom';
20
21
 
21
22
  import * as C from './utils/constants';
23
+ import {COLUMN_NAME, COLUMNS_NAMES} from './utils/constants';
22
24
  import * as type from './utils/types';
23
- import {calculateSelected, extractColInfo, scaleActivity} from './utils/misc';
24
- import {MONOMER_POSITION_PROPERTIES, MonomerPosition, MostPotentResidues, SELECTION_MODE} from './viewers/sar-viewer';
25
+ import {PeptidesSettings} from './utils/types';
26
+ import {
27
+ areParametersEqual,
28
+ calculateSelected,
29
+ getSelectionBitset,
30
+ highlightMonomerPosition,
31
+ initSelection,
32
+ modifySelection,
33
+ mutationCliffsToMaskInfo,
34
+ scaleActivity,
35
+ } from './utils/misc';
36
+ import {ISARViewer, MonomerPosition, MostPotentResidues, SARViewer} from './viewers/sar-viewer';
25
37
  import * as CR from './utils/cell-renderer';
26
38
  import {mutationCliffsWidget} from './widgets/mutation-cliffs';
27
- import {getDistributionWidget} from './widgets/distribution';
28
- import {ClusterTypeStats, MonomerPositionStats, PositionStats} from './utils/statistics';
29
- import {LogoSummaryTable} from './viewers/logo-summary';
39
+ import {getDistributionWidget, PeptideViewer} from './widgets/distribution';
40
+ import {CLUSTER_TYPE, ILogoSummaryTable, LogoSummaryTable, LST_PROPERTIES} from './viewers/logo-summary';
30
41
  import {getSettingsDialog} from './widgets/settings';
31
42
  import {_package, getTreeHelperInstance} from './package';
32
- import {calculateClusterStatistics, calculateMonomerPositionStatistics, findMutations} from './utils/algorithms';
43
+ import {calculateMonomerPositionStatistics} from './utils/algorithms';
33
44
  import {createDistanceMatrixWorker} from './utils/worker-creator';
34
45
  import {getSelectionWidget} from './widgets/selection';
35
46
 
36
- import {MmDistanceFunctionsNames} from '@datagrok-libraries/ml/src/macromolecule-distance-functions';
37
- import {BitArrayMetrics} from '@datagrok-libraries/ml/src/typed-metrics';
47
+ import {mmDistanceFunctions, MmDistanceFunctionsNames} from '@datagrok-libraries/ml/src/macromolecule-distance-functions';
38
48
  import {DimReductionMethods, ITSNEOptions, IUMAPOptions} from '@datagrok-libraries/ml/src/reduce-dimensionality';
39
49
  import {showMonomerTooltip} from './utils/tooltips';
50
+ import {AggregationColumns, MonomerPositionStats} from './utils/statistics';
51
+ import {splitAlignedSequences} from '@datagrok-libraries/bio/src/utils/splitter';
40
52
 
41
- export enum CLUSTER_TYPE {
42
- ORIGINAL = 'original',
43
- CUSTOM = 'custom',
44
- };
45
- export type ClusterType = `${CLUSTER_TYPE}`;
46
53
  export enum VIEWER_TYPE {
47
54
  MONOMER_POSITION = 'Monomer-Position',
48
55
  MOST_POTENT_RESIDUES = 'Most Potent Residues',
49
56
  LOGO_SUMMARY_TABLE = 'Logo Summary Table',
50
57
  DENDROGRAM = 'Dendrogram',
51
- };
58
+ }
59
+
60
+ export type CachedWebLogoTooltip = { bar: string, tooltip: HTMLDivElement | null };
52
61
 
62
+ /**
63
+ * Peptides model class
64
+ * Controls analysis settings and initialization, collaborative filtering and property panel.
65
+ */
53
66
  export class PeptidesModel {
67
+ // Tag for storing model instance in DataFrame temp
54
68
  static modelName = 'peptidesModel';
55
69
 
70
+ // Prevents firing bitset changed event if not initialized
56
71
  isBitsetChangedInitialized = false;
72
+ // Prevents overriding user selection
57
73
  isUserChangedSelection = true;
58
74
 
59
75
  df: DG.DataFrame;
60
- _monomerPositionStats?: MonomerPositionStats;
61
- _clusterStats?: ClusterTypeStats;
62
- _mutationCliffsSelection!: type.Selection;
63
- _invariantMapSelection!: type.Selection;
64
- _clusterSelection!: type.Selection;
65
- _mutationCliffs: type.MutationCliffs | null = null;
76
+ _dm: DistanceMatrix | null = null;
77
+ // Prevents redundant intialization
66
78
  isInitialized = false;
67
- _analysisView?: DG.TableView;
68
-
69
-
70
- _settings!: type.PeptidesSettings;
79
+ // Prevents duplicating ribbon
71
80
  isRibbonSet = false;
72
-
73
- _cp?: SeqPalette;
74
- headerSelectedMonomers: type.SelectionStats = {};
81
+ // Cached stats for WebLogo selection for efficient rendering
82
+ webLogoSelectedMonomers: type.SelectionStats = {};
83
+ // WebLogo bounds used for interactivity (e.g. tooltips, selection)
75
84
  webLogoBounds: CR.WebLogoBounds = {};
76
- cachedWebLogoTooltip: {bar: string, tooltip: HTMLDivElement | null} = {bar: '', tooltip: null};
77
- _alphabet?: string;
78
- _dm!: DistanceMatrix;
85
+ // Cached WebLogo tooltip. Because tooltip is requested for each mouse movement, it is cached unless mouse entered
86
+ // bounds of other monomer in WebLogo
87
+ cachedWebLogoTooltip: CachedWebLogoTooltip = {bar: '', tooltip: null};
88
+ // Prevents from redundant grid processing
79
89
  _layoutEventInitialized = false;
80
-
90
+ // Stores subscriptions to remove after analysis is closed
81
91
  subs: rxjs.Subscription[] = [];
92
+ // Prevents from redundant unhilight operation
82
93
  isHighlighting: boolean = false;
83
- latestSelectionItem: (type.SelectionItem & {kind: SELECTION_MODE | 'Cluster'}) | null = null;
94
+ // Fires bitset changed to properly render selection in scroller bar
84
95
  controlFire: boolean = false;
85
-
96
+ // Indicates the source of accordion construction
97
+ accordionSource: VIEWER_TYPE | null = null;
98
+ // sequence space viewer
99
+ _sequenceSpaceViewer: DG.ScatterPlotViewer | null = null;
100
+ /**
101
+ * @param dataFrame - DataFrame to use for analysis
102
+ */
86
103
  private constructor(dataFrame: DG.DataFrame) {
87
104
  this.df = dataFrame;
88
105
  }
89
106
 
90
- static getInstance(dataFrame: DG.DataFrame): PeptidesModel {
91
- dataFrame.temp[PeptidesModel.modelName] ??= new PeptidesModel(dataFrame);
92
- (dataFrame.temp[PeptidesModel.modelName] as PeptidesModel).init();
93
- return dataFrame.temp[PeptidesModel.modelName] as PeptidesModel;
94
- }
95
-
96
- get id(): string {
97
- const id = this.df.getTag(C.TAGS.UUID);
98
- if (id === null || id === '')
99
- throw new Error('PeptidesError: UUID is not defined');
100
-
101
- return id;
102
- }
103
-
104
- get monomerPositionStats(): MonomerPositionStats {
105
- this._monomerPositionStats ??= calculateMonomerPositionStatistics(this.df, this.positionColumns.toArray());
106
- return this._monomerPositionStats!;
107
- }
107
+ // Monomer-Position statistics cache
108
+ _monomerPositionStats: MonomerPositionStats | null = null;
108
109
 
109
- set monomerPositionStats(mps: MonomerPositionStats) {
110
- this._monomerPositionStats = mps;
111
- }
112
-
113
- get positionColumns(): wu.WuIterable<DG.Column> {
114
- return wu(this.df.columns.byTags({[C.TAGS.POSITION_COL]: `${true}`}));
115
- }
116
-
117
- get alphabet(): string {
118
- const col = this.df.getCol(this.settings.sequenceColumnName!);
119
- return col.getTag(bioTAGS.alphabet);
120
- }
121
-
122
- get mutationCliffs(): type.MutationCliffs | null {
123
- return this._mutationCliffs!;
124
- }
110
+ /**
111
+ * @return - Monomer-Position statistics
112
+ */
113
+ get monomerPositionStats(): MonomerPositionStats | null {
114
+ if (this._monomerPositionStats !== null) {
115
+ return this._monomerPositionStats;
116
+ }
125
117
 
126
- set mutationCliffs(si: type.MutationCliffs | null) {
127
- this._mutationCliffs = si;
128
- }
129
118
 
130
- get clusterStats(): ClusterTypeStats {
131
- this._clusterStats ??= calculateClusterStatistics(this.df, this.settings.clustersColumnName!,
132
- this.customClusters.toArray());
133
- return this._clusterStats!;
134
- }
119
+ const scaledActivityColumn = this.getScaledActivityColumn();
120
+ if (this.positionColumns === null || scaledActivityColumn === null) {
121
+ return null;
122
+ }
135
123
 
136
- set clusterStats(clusterStats: ClusterTypeStats) {
137
- this._clusterStats = clusterStats;
138
- }
139
124
 
140
- get cp(): SeqPalette {
141
- this._cp ??= pickUpPalette(this.df.getCol(this.settings.sequenceColumnName!));
142
- return this._cp;
125
+ this._monomerPositionStats ??= calculateMonomerPositionStatistics(scaledActivityColumn,
126
+ this.df.filter, this.positionColumns);
127
+ return this._monomerPositionStats;
143
128
  }
144
129
 
145
- set cp(_cp: SeqPalette) {
146
- this._cp = _cp;
147
- }
130
+ // Analysis Table View
131
+ _analysisView?: DG.TableView;
148
132
 
133
+ /**
134
+ * @return - Analysis table view
135
+ */
149
136
  get analysisView(): DG.TableView {
150
137
  if (this._analysisView === undefined) {
151
- this._analysisView = wu(grok.shell.tableViews).find(({dataFrame}) => dataFrame?.getTag(C.TAGS.UUID) === this.id);
152
- if (this._analysisView === undefined) {
138
+ this._analysisView = wu(grok.shell.tableViews).find(({dataFrame}) => dataFrame?.getTag(DG.TAGS.ID) === this.id);
139
+ if (typeof this._analysisView === 'undefined') {
153
140
  this._analysisView = grok.shell.addTableView(this.df);
154
- const posCols = this.positionColumns.toArray().map((col) => col.name);
155
-
156
- for (let colIdx = 1; colIdx < this._analysisView.grid.columns.length; ++colIdx) {
157
- const gridCol = this._analysisView.grid.columns.byIndex(colIdx)!;
158
- gridCol.visible =
159
- posCols.includes(gridCol.column!.name) || (gridCol.column!.name === C.COLUMNS_NAMES.ACTIVITY_SCALED);
160
- }
161
141
  }
162
142
  }
163
143
 
164
- if (this.df.getTag(C.TAGS.MULTIPLE_VIEWS) !== '1' && !this._layoutEventInitialized)
144
+ if (this.df.getTag(C.TAGS.MULTIPLE_VIEWS) !== '1' && !this._layoutEventInitialized) {
165
145
  grok.shell.v = this._analysisView;
166
-
167
- this._analysisView.grid.invalidate();
168
- return this._analysisView;
169
- }
170
-
171
- get mutationCliffsSelection(): type.Selection {
172
- const tagSelection = this.df.getTag(C.TAGS.MUTATION_CLIFFS_SELECTION) ?? this.df.getTag(C.TAGS.SELECTION);
173
- if (tagSelection === null)
174
- this.initMutationCliffsSelection({notify: false});
175
- this._mutationCliffsSelection ??= JSON.parse(tagSelection ?? this.df.getTag(C.TAGS.MUTATION_CLIFFS_SELECTION) ?? this.df.getTag(C.TAGS.SELECTION)!);
176
- return this._mutationCliffsSelection;
177
- }
178
-
179
- set mutationCliffsSelection(selection: type.Selection) {
180
- this._mutationCliffsSelection = selection;
181
- // TODO: Remove in 1.14.0
182
- this.df.setTag(C.TAGS.SELECTION, JSON.stringify(selection));
183
- this.df.setTag(C.TAGS.MUTATION_CLIFFS_SELECTION, JSON.stringify(selection));
184
- this.fireBitsetChanged();
185
-
186
- const mpViewer = this.findViewer(VIEWER_TYPE.MONOMER_POSITION) as MonomerPosition | null;
187
- mpViewer?.viewerGrid.invalidate();
188
- const mprViewer = this.findViewer(VIEWER_TYPE.MOST_POTENT_RESIDUES) as MostPotentResidues | null;
189
- mprViewer?.viewerGrid.invalidate();
190
-
191
- this.analysisView.grid.invalidate();
192
- }
193
-
194
- get invariantMapSelection(): type.Selection {
195
- const tagSelection = this.df.getTag(C.TAGS.INVARIANT_MAP_SELECTION) ?? this.df.getTag(C.TAGS.FILTER);
196
- if (tagSelection === null)
197
- this.initInvariantMapSelection({notify: false});
198
- this._invariantMapSelection ??= JSON.parse(tagSelection ?? this.df.getTag(C.TAGS.INVARIANT_MAP_SELECTION) ?? this.df.getTag(C.TAGS.FILTER)!);
199
- return this._invariantMapSelection;
200
- }
201
-
202
- set invariantMapSelection(selection: type.Selection) {
203
- this._invariantMapSelection = selection;
204
- this.df.setTag(C.TAGS.INVARIANT_MAP_SELECTION, JSON.stringify(selection));
205
- // TODO: Remove in 1.14.0
206
- this.df.setTag(C.TAGS.FILTER, JSON.stringify(selection));
207
- this.fireBitsetChanged();
208
- this.analysisView.grid.invalidate();
209
- }
210
-
211
- get clusterSelection(): type.Selection {
212
- const tagSelection = this.df.getTag(C.TAGS.CLUSTER_SELECTION);
213
- if (tagSelection === null)
214
- this.initClusterSelection({notify: false});
215
- this._clusterSelection ??= JSON.parse(tagSelection ?? this.df.getTag(C.TAGS.CLUSTER_SELECTION)!);
216
- // TODO: Remove in 1.14.0
217
- if (Array.isArray(this._clusterSelection)) {
218
- const newSelection: type.Selection = {};
219
- newSelection[CLUSTER_TYPE.ORIGINAL] = [];
220
- newSelection[CLUSTER_TYPE.CUSTOM] = [];
221
- for (const cluster of this._clusterSelection) {
222
- if (wu(this.customClusters).some((col) => col.name === cluster))
223
- newSelection[CLUSTER_TYPE.CUSTOM].push(cluster);
224
- else
225
- newSelection[CLUSTER_TYPE.ORIGINAL].push(cluster);
226
- }
227
- this._clusterSelection = newSelection;
228
146
  }
229
- return this._clusterSelection;
230
- }
231
-
232
- set clusterSelection(selection: type.Selection) {
233
- this._clusterSelection = selection;
234
- this.df.tags[C.TAGS.CLUSTER_SELECTION] = JSON.stringify(selection);
235
- this.fireBitsetChanged();
236
- this.analysisView.grid.invalidate();
237
- }
238
-
239
- get splitByPos(): boolean {
240
- const splitByPosFlag = (this.df.tags['distributionSplit'] || '00')[0];
241
- return splitByPosFlag === '1' ? true : false;
242
- }
243
147
 
244
- set splitByPos(flag: boolean) {
245
- const splitByMonomerFlag = (this.df.tags['distributionSplit'] || '00')[1];
246
- this.df.tags['distributionSplit'] = `${flag ? 1 : 0}${splitByMonomerFlag}`;
247
- }
248
-
249
- get splitByMonomer(): boolean {
250
- const splitByPosFlag = (this.df.tags['distributionSplit'] || '00')[1];
251
- return splitByPosFlag === '1' ? true : false;
252
- }
253
148
 
254
- set splitByMonomer(flag: boolean) {
255
- const splitByMonomerFlag = (this.df.tags['distributionSplit'] || '00')[0];
256
- this.df.tags['distributionSplit'] = `${splitByMonomerFlag}${flag ? 1 : 0}`;
149
+ this._analysisView.grid.invalidate();
150
+ return this._analysisView;
257
151
  }
258
152
 
259
- get isMutationCliffsSelectionEmpty(): boolean {
260
- for (const monomerList of Object.values(this.mutationCliffsSelection)) {
261
- if (monomerList.length !== 0)
262
- return false;
263
- }
264
- return true;
265
- }
153
+ // Peptides analysis settings
154
+ _settings: type.PeptidesSettings | null = null;
155
+ _sequenceSpaceCols: string[] = [];
266
156
 
267
- get isInvariantMapSelectionEmpty(): boolean {
268
- for (const monomerList of Object.values(this.invariantMapSelection)) {
269
- if (monomerList.length !== 0)
270
- return false;
157
+ /**
158
+ * @return - Peptides analysis settings
159
+ */
160
+ get settings(): type.PeptidesSettings | null {
161
+ const settingsStr = this.df.getTag(C.TAGS.SETTINGS);
162
+ if (settingsStr == null) {
163
+ return null;
271
164
  }
272
- return true;
273
- }
274
-
275
- get isClusterSelectionEmpty(): boolean {
276
- return (this.clusterSelection[CLUSTER_TYPE.ORIGINAL].length + this.clusterSelection[CLUSTER_TYPE.CUSTOM].length) === 0;
277
- }
278
165
 
279
- get customClusters(): wu.WuIterable<DG.Column<boolean>> {
280
- const query: { [key: string]: string } = {};
281
- query[C.TAGS.CUSTOM_CLUSTER] = '1';
282
- return wu(this.df.columns.byTags(query));
283
- }
284
166
 
285
- get settings(): type.PeptidesSettings {
286
- this._settings ??= JSON.parse(this.df.getTag('settings')!);
287
- return this._settings;
167
+ this._settings ??= JSON.parse(settingsStr);
168
+ return this._settings!;
288
169
  }
289
170
 
290
- set settings(s: type.PeptidesSettings) {
291
- const newSettingsEntries = Object.entries(s);
171
+ /**
172
+ * @param s - Peptides analysis settings
173
+ */
174
+ set settings(s: type.PartialPeptidesSettings) {
175
+ const newSettingsEntries = Object.entries(s) as ([keyof type.PeptidesSettings, never])[];
176
+ // Holds updated settings categories
292
177
  const updateVars: Set<string> = new Set();
293
178
  for (const [key, value] of newSettingsEntries) {
294
- this._settings[key as keyof type.PeptidesSettings] = value as any;
179
+ this.settings![key] = value;
295
180
  switch (key) {
296
181
  case 'activityColumnName':
297
- case 'scaling':
182
+ case 'activityScaling':
298
183
  updateVars.add('activity');
299
- updateVars.add('mutationCliffs');
300
184
  updateVars.add('stats');
301
185
  break;
302
- case 'columns':
303
- updateVars.add('grid');
304
- break;
305
- case 'maxMutations':
306
- case 'minActivityDelta':
307
- updateVars.add('mutationCliffs');
308
- break;
309
186
  case 'showDendrogram':
310
187
  updateVars.add('dendrogram');
311
188
  break;
@@ -318,164 +195,441 @@ export class PeptidesModel {
318
195
  case 'showMostPotentResidues':
319
196
  updateVars.add('mostPotentResidues');
320
197
  break;
198
+ case 'columns':
199
+ updateVars.add('columns');
200
+ break;
201
+ case 'sequenceSpaceParams':
202
+ updateVars.add('sequenceSpaceParams');
321
203
  }
322
204
  }
205
+ // Write updated settings
323
206
  this.df.setTag('settings', JSON.stringify(this._settings));
324
- let updateViewersData = false;
207
+ if (!this.isInitialized) {
208
+ return;
209
+ }
210
+
211
+
212
+ // Apply new settings
325
213
  for (const variable of updateVars) {
326
214
  switch (variable) {
327
215
  case 'activity':
328
216
  this.createScaledCol();
329
- updateViewersData = true;
330
- break;
331
- case 'mutationCliffs':
332
- this.updateMutationCliffs();
333
217
  break;
334
218
  case 'stats':
335
- this.monomerPositionStats = calculateMonomerPositionStatistics(this.df, this.positionColumns.toArray());
336
- this.clusterStats = calculateClusterStatistics(this.df, this.settings.clustersColumnName!,
337
- this.customClusters.toArray());
338
- updateViewersData = true;
339
- break;
340
- case 'grid':
341
- this.setGridProperties();
342
- const lstViewer = this.findViewer(VIEWER_TYPE.LOGO_SUMMARY_TABLE) as LogoSummaryTable | null;
343
- lstViewer?.createLogoSummaryTableGrid();
344
- lstViewer?.render();
219
+ this.webLogoSelection = initSelection(this.positionColumns!);
220
+ this.webLogoBounds = {};
221
+ this.cachedWebLogoTooltip = {
222
+ bar: '',
223
+ tooltip: null,
224
+ };
225
+ // Invalidate monomer-position statistics. The next time it gets accessed with getter, it will be recalculated
226
+ this._monomerPositionStats = null;
345
227
  break;
346
228
  case 'dendrogram':
347
- this.settings.showDendrogram ? this.addDendrogram() : this.closeViewer(VIEWER_TYPE.DENDROGRAM);
229
+ this.settings!.showDendrogram ? this.addDendrogram() : this.closeViewer(VIEWER_TYPE.DENDROGRAM);
348
230
  break;
349
231
  case 'logoSummaryTable':
350
- this.settings.showLogoSummaryTable ? this.addLogoSummaryTable() :
232
+ this.settings!.showLogoSummaryTable ? this.addLogoSummaryTable() :
351
233
  this.closeViewer(VIEWER_TYPE.LOGO_SUMMARY_TABLE);
352
234
  break;
353
235
  case 'monomerPosition':
354
- this.settings.showMonomerPosition ? this.addMonomerPosition() :
236
+ this.settings!.showMonomerPosition ? this.addMonomerPosition() :
355
237
  this.closeViewer(VIEWER_TYPE.MONOMER_POSITION);
356
238
  break;
357
239
  case 'mostPotentResidues':
358
- this.settings.showMostPotentResidues ? this.addMostPotentResidues() :
240
+ this.settings!.showMostPotentResidues ? this.addMostPotentResidues() :
359
241
  this.closeViewer(VIEWER_TYPE.MOST_POTENT_RESIDUES);
360
242
  break;
243
+ case 'columns':
244
+ const lst = this.findViewer(VIEWER_TYPE.LOGO_SUMMARY_TABLE) as LogoSummaryTable;
245
+ lst._viewerGrid = null;
246
+ lst._logoSummaryTable = null;
247
+ lst.render();
248
+ const mpr = this.findViewer(VIEWER_TYPE.MOST_POTENT_RESIDUES) as LogoSummaryTable;
249
+ mpr._viewerGrid = null;
250
+ mpr.render();
251
+ break;
252
+ case 'sequenceSpaceParams':
253
+ this.addSequenceSpace();
254
+ break;
361
255
  }
362
256
  }
257
+ }
363
258
 
364
- //TODO: handle settings change
365
- const mpViewer = this.findViewer(VIEWER_TYPE.MONOMER_POSITION) as MonomerPosition | null;
366
- if (updateViewersData)
367
- mpViewer?.createMonomerPositionGrid();
368
- mpViewer?.render();
369
- const mprViewer = this.findViewer(VIEWER_TYPE.MOST_POTENT_RESIDUES) as MostPotentResidues | null;
370
- if (updateViewersData)
371
- mprViewer?.createMostPotentResiduesGrid();
372
- mprViewer?.render();
373
- const lstViewer = this.findViewer(VIEWER_TYPE.LOGO_SUMMARY_TABLE) as LogoSummaryTable | null;
374
- if (updateViewersData)
375
- lstViewer?.createLogoSummaryTableGrid();
376
- lstViewer?.render();
259
+ // Current Monomer-Position selection that came from WebLogo in header
260
+ _webLogoSelection: type.Selection | null = null;
261
+
262
+ /**
263
+ * @return - Current Monomer-Position selection that came from WebLogo in header
264
+ */
265
+ get webLogoSelection(): type.Selection {
266
+ const tagSelection = this.df.getTag(`${C.SUFFIXES.WL}${C.TAGS.INVARIANT_MAP_SELECTION}`);
267
+ this._webLogoSelection ??= tagSelection === null && this.positionColumns !== null ?
268
+ initSelection(this.positionColumns) : JSON.parse(tagSelection ?? `{}`);
269
+ return this._webLogoSelection!;
377
270
  }
378
271
 
379
- get identityTemplate(): string {
380
- return this.df.getTag(C.TAGS.IDENTITY_TEMPLATE) ?? '';
272
+ /**
273
+ * @param selection - Current Monomer-Position selection that came from WebLogo in header
274
+ */
275
+ set webLogoSelection(selection: type.Selection) {
276
+ this._webLogoSelection = selection;
277
+ this.df.setTag(`${C.SUFFIXES.WL}${C.TAGS.INVARIANT_MAP_SELECTION}`, JSON.stringify(selection));
278
+ this.fireBitsetChanged(null);
279
+ this.analysisView.grid.invalidate();
381
280
  }
382
281
 
383
- set identityTemplate(template: string) {
384
- this.df.setTag(C.TAGS.IDENTITY_TEMPLATE, template);
282
+ /**
283
+ * @return - Array of columns that represent monomers at specific position in sequences
284
+ */
285
+ get positionColumns(): DG.Column<string>[] | null {
286
+ const positionColumns = wu(this.df.columns.byTags({[C.TAGS.POSITION_COL]: `${true}`})).toArray();
287
+ if (positionColumns.length === 0) {
288
+ return null;
289
+ }
290
+
291
+
292
+ return positionColumns;
385
293
  }
386
294
 
387
- async updateMutationCliffs(notify: boolean = true): Promise<void> {
388
- const scaledActivityCol: DG.Column<number> = this.df.getCol(C.COLUMNS_NAMES.ACTIVITY_SCALED);
389
- //TODO: set categories ordering the same to share compare indexes instead of strings
390
- const monomerCols: type.RawColumn[] = this.df.columns.bySemTypeAll(C.SEM_TYPES.MONOMER).map(extractColInfo);
391
- const targetCol = typeof this.settings.targetColumnName !== 'undefined' ?
392
- extractColInfo(this.df.getCol(this.settings.targetColumnName)) : null;
393
- let mpViewer = this.findViewer(VIEWER_TYPE.MONOMER_POSITION) as MonomerPosition | null;
394
- const currentTarget = mpViewer?.getProperty(MONOMER_POSITION_PROPERTIES.TARGET)?.get(mpViewer) as string | undefined;
395
- const targetOptions = {targetCol: targetCol, currentTarget: currentTarget};
396
- const mutationCliffs = await findMutations(scaledActivityCol.getRawData(), monomerCols, this.settings, targetOptions);
397
- if (notify)
398
- this.mutationCliffs = mutationCliffs;
399
- else
400
- this._mutationCliffs = mutationCliffs;
295
+ /**
296
+ * @return - DataFrame ID
297
+ */
298
+ get id(): string {
299
+ return this.df.getTag(DG.TAGS.ID)!;
300
+ }
401
301
 
402
- mpViewer ??= this.findViewer(VIEWER_TYPE.MONOMER_POSITION) as MonomerPosition | null;
403
- mpViewer?.render(true);
404
- const mostPotentViewer = this.findViewer(VIEWER_TYPE.MOST_POTENT_RESIDUES) as MostPotentResidues | null;
405
- mostPotentViewer?.render(true);
302
+ /**
303
+ * @return - Sequence alphabet
304
+ */
305
+ get alphabet(): string {
306
+ const col = this.df.getCol(this.settings!.sequenceColumnName);
307
+ return col.getTag(bioTAGS.alphabet);
406
308
  }
407
309
 
408
- buildSplitSeqDf(): DG.DataFrame {
409
- const sequenceCol = this.df.getCol(this.settings.sequenceColumnName!);
410
- const splitSeqDf = splitAlignedSequences(sequenceCol);
310
+ /**
311
+ * Creates an instance of PeptidesModel or returns existing if present
312
+ * @param dataFrame - DataFrame to use for analysis
313
+ * @return - PeptidesModel instance
314
+ */
315
+ static getInstance(dataFrame: DG.DataFrame): PeptidesModel {
316
+ if (dataFrame.columns.contains(C.COLUMNS_NAMES.ACTIVITY_SCALED) &&
317
+ !dataFrame.columns.contains(C.COLUMNS_NAMES.ACTIVITY)) {
318
+ dataFrame.getCol(C.COLUMNS_NAMES.ACTIVITY_SCALED).name = C.COLUMNS_NAMES.ACTIVITY;
319
+ }
411
320
 
412
- return splitSeqDf;
321
+
322
+ dataFrame.temp[PeptidesModel.modelName] ??= new PeptidesModel(dataFrame);
323
+ return dataFrame.temp[PeptidesModel.modelName] as PeptidesModel;
413
324
  }
414
325
 
415
- getCompoundBitset(): DG.BitSet {
326
+ /**
327
+ * Modifies WebLogo selection. If shift and ctrl keys are both pressed, it removes WebLogo from
328
+ * selection. If only shift key is pressed, it adds WebLogo to selection. If only ctrl key is pressed, it
329
+ * changes WebLogo presence in selection. If none of the keys is pressed, it sets the WebLogo as the
330
+ * only selected one.
331
+ * @param monomerPosition - cluster to modify selection with.
332
+ * @param options - selection options.
333
+ * @param notify - flag indicating if bitset changed event should fire.
334
+ */
335
+ modifyWebLogoSelection(monomerPosition: type.SelectionItem, options: type.SelectionOptions = {
336
+ shiftPressed: false,
337
+ ctrlPressed: false,
338
+ }, notify: boolean = true): void {
339
+ if (notify) {
340
+ this.webLogoSelection = modifySelection(this.webLogoSelection, monomerPosition, options);
341
+ } else {
342
+ this._webLogoSelection = modifySelection(this.webLogoSelection, monomerPosition, options);
343
+ }
344
+ }
345
+
346
+ /**
347
+ * @return - Bitset of visible selection
348
+ */
349
+ getVisibleSelection(): DG.BitSet {
416
350
  return this.df.selection.clone().and(this.df.filter);
417
351
  }
418
352
 
353
+ /**
354
+ * @return - Accordion with analysis info based on current selection
355
+ */
419
356
  createAccordion(): DG.Accordion | null {
420
357
  const trueModel: PeptidesModel | undefined = grok.shell.t?.temp[PeptidesModel.modelName];
421
- if (!trueModel)
358
+ if (!trueModel) {
422
359
  return null;
360
+ }
423
361
 
424
- const acc = ui.accordion();
362
+
363
+ const acc = ui.accordion('Peptides analysis panel');
425
364
  acc.root.style.width = '100%';
426
- const filterAndSelectionBs = trueModel.getCompoundBitset();
427
- const filteredTitlePart = trueModel.df.filter.anyFalse ? ` among ${trueModel.df.filter.trueCount} filtered` : '';
428
- acc.addTitle(ui.h1(`${filterAndSelectionBs.trueCount} selected rows${filteredTitlePart}`));
365
+ const filterAndSelectionBs = trueModel.getVisibleSelection();
366
+ const filteredTitlePart = trueModel.df.filter.anyFalse ? ` among ${trueModel.df.filter.trueCount} filtered` :
367
+ '';
368
+ const getSelectionString = (selection: type.Selection): string => {
369
+ const selectedMonomerPositions: string[] = [];
370
+ for (const [pos, monomerList] of Object.entries(selection)) {
371
+ for (const monomer of monomerList) {
372
+ selectedMonomerPositions.push(`${pos}:${monomer}`);
373
+ }
374
+ }
375
+ return selectedMonomerPositions.join(', ');
376
+ };
377
+
378
+ // Logo Summary Table viewer selection overview
379
+ const trueLSTViewer = trueModel.findViewer(VIEWER_TYPE.LOGO_SUMMARY_TABLE) as LogoSummaryTable | null;
380
+ const selectionDescription: HTMLElement[] = [];
381
+ const selectedClusters: string = (trueLSTViewer === null ? [] :
382
+ trueLSTViewer.clusterSelection[CLUSTER_TYPE.ORIGINAL].concat(trueLSTViewer.clusterSelection[CLUSTER_TYPE.CUSTOM]))
383
+ .join(', ');
384
+ if (selectedClusters.length !== 0) {
385
+ selectionDescription.push(ui.h1('Logo summary table selection'));
386
+ selectionDescription.push(ui.divText(`Selected clusters: ${selectedClusters}`));
387
+ }
388
+
389
+ // Monomer-Position viewer selection overview
390
+ const trueMPViewer = trueModel.findViewer(VIEWER_TYPE.MONOMER_POSITION) as MonomerPosition | null;
391
+ const selectedMonomerPositions = getSelectionString(trueMPViewer?.invariantMapSelection ?? {});
392
+ const selectedMutationCliffs = getSelectionString(trueMPViewer?.mutationCliffsSelection ?? {});
393
+ if (selectedMonomerPositions.length !== 0 || selectedMutationCliffs.length !== 0) {
394
+ selectionDescription.push(ui.h1('Monomer-Position viewer selection'));
395
+ }
396
+
397
+
398
+ if (selectedMonomerPositions.length !== 0) {
399
+ selectionDescription.push(ui.divText(`Selected monomer-positions: ${selectedMonomerPositions}`));
400
+ }
401
+
402
+
403
+ if (selectedMutationCliffs.length !== 0) {
404
+ selectionDescription.push(ui.divText(`Selected mutation cliffs pairs: ${selectedMutationCliffs}`));
405
+ }
406
+
407
+
408
+ // Most Potent Residues viewer selection overview
409
+ const trueMPRViewer = trueModel.findViewer(VIEWER_TYPE.MOST_POTENT_RESIDUES) as MostPotentResidues | null;
410
+ const selectedMPRMonomerPositions = getSelectionString(trueMPRViewer?.mutationCliffsSelection ?? {});
411
+ if (selectedMPRMonomerPositions.length !== 0) {
412
+ selectionDescription.push(ui.h1('Most Potent Residues viewer selection'));
413
+ selectionDescription.push(ui.divText(`Selected monomer-positions: ${selectedMPRMonomerPositions}`));
414
+ }
415
+
416
+ // WebLogo selection overview
417
+ const selectedMonomers = getSelectionString(trueModel.webLogoSelection);
418
+ if (selectedMonomers.length !== 0) {
419
+ selectionDescription.push(ui.h1('WebLogo selection'));
420
+ selectionDescription.push(ui.divText(`Selected monomers: ${selectedMonomers}`));
421
+ }
422
+
423
+ const descritionsHost = ui.div(ui.divV(selectionDescription));
424
+ acc.addTitle(ui.divV([
425
+ ui.h1(`${filterAndSelectionBs.trueCount} selected rows${filteredTitlePart}`),
426
+ descritionsHost,
427
+ ], 'css-gap-small'));
428
+
429
429
  if (filterAndSelectionBs.anyTrue) {
430
430
  acc.addPane('Actions', () => {
431
431
  const newView = ui.label('New view');
432
432
  $(newView).addClass('d4-link-action');
433
433
  newView.onclick = (): string => trueModel.createNewView();
434
- newView.onmouseover =
435
- (ev): void => ui.tooltip.show('Creates a new view from current selection', ev.clientX + 5, ev.clientY + 5);
434
+ newView.onmouseover = (ev): void =>
435
+ ui.tooltip.show('Creates a new view from current selection', ev.clientX + 5, ev.clientY + 5);
436
+ if (trueLSTViewer === null) {
437
+ return ui.divV([newView]);
438
+ }
439
+
440
+
436
441
  const newCluster = ui.label('New cluster');
437
442
  $(newCluster).addClass('d4-link-action');
438
443
  newCluster.onclick = (): void => {
439
- const lstViewer = trueModel.findViewer(VIEWER_TYPE.LOGO_SUMMARY_TABLE) as LogoSummaryTable | null;
440
- if (lstViewer === null)
444
+ if (trueLSTViewer === null) {
441
445
  throw new Error('Logo summary table viewer is not found');
442
- lstViewer.clusterFromSelection();
446
+ }
447
+
448
+
449
+ trueLSTViewer.clusterFromSelection();
443
450
  };
444
- newCluster.onmouseover =
445
- (ev): void => ui.tooltip.show('Creates a new cluster from selection', ev.clientX + 5, ev.clientY + 5);
451
+ newCluster.onmouseover = (ev): void =>
452
+ ui.tooltip.show('Creates a new cluster from selection', ev.clientX + 5, ev.clientY + 5);
453
+
446
454
  const removeCluster = ui.label('Remove cluster');
447
455
  $(removeCluster).addClass('d4-link-action');
448
456
  removeCluster.onclick = (): void => {
449
457
  const lstViewer = trueModel.findViewer(VIEWER_TYPE.LOGO_SUMMARY_TABLE) as LogoSummaryTable | null;
450
- if (lstViewer === null)
458
+ if (lstViewer === null) {
451
459
  throw new Error('Logo summary table viewer is not found');
460
+ }
461
+
462
+
452
463
  lstViewer.removeCluster();
453
464
  };
454
- removeCluster.onmouseover =
455
- (ev): void => ui.tooltip.show('Removes currently selected custom cluster', ev.clientX + 5, ev.clientY + 5);
456
- removeCluster.style.visibility = trueModel.clusterSelection[CLUSTER_TYPE.CUSTOM].length === 0 ? 'hidden' : 'visible';
465
+ removeCluster.onmouseover = (ev): void =>
466
+ ui.tooltip.show('Removes currently selected custom cluster', ev.clientX + 5, ev.clientY + 5);
467
+ removeCluster.style.visibility = trueLSTViewer.clusterSelection[CLUSTER_TYPE.CUSTOM].length === 0 ? 'hidden' :
468
+ 'visible';
469
+
457
470
  return ui.divV([newView, newCluster, removeCluster]);
471
+ }, true);
472
+ }
473
+
474
+ // Get the source of the bitset change and find viewers that share the same parameters as source
475
+ let requestSource: SARViewer | LogoSummaryTable | PeptidesSettings | null = trueModel.settings;
476
+ const viewers: (PeptideViewer | PeptidesSettings | null)[] = [trueMPViewer, trueMPRViewer, trueLSTViewer]
477
+ .filter((v) => {
478
+ if (v === null) {
479
+ return false;
480
+ }
481
+
482
+
483
+ if (v.type !== this.accordionSource) {
484
+ return true;
485
+ }
486
+
487
+
488
+ requestSource = v;
489
+ return false;
458
490
  });
491
+
492
+ if (requestSource === null) {
493
+ throw new Error('PeptidesError: Model is the source of accordion but is not initialized');
494
+ }
495
+
496
+
497
+ if (requestSource !== trueModel.settings) {
498
+ viewers.push(trueModel.settings);
499
+ }
500
+
501
+
502
+ const notEmpty = (v: PeptideViewer | PeptidesSettings | null): v is PeptideViewer | PeptidesSettings =>
503
+ v !== null && areParametersEqual(requestSource!, v) && (v !== trueModel.settings || trueModel.isInitialized);
504
+ const panelDataSources = viewers.filter(notEmpty);
505
+ panelDataSources.push(requestSource);
506
+ const combinedBitset: DG.BitSet | null = DG.BitSet.create(trueModel.df.rowCount);
507
+ for (const panelDataSource of panelDataSources) {
508
+ const bitset =
509
+ (panelDataSource === this.settings) ? getSelectionBitset(this.webLogoSelection, this.monomerPositionStats!) :
510
+ (panelDataSource instanceof LogoSummaryTable) ?
511
+ getSelectionBitset(panelDataSource.clusterSelection, panelDataSource.clusterStats) :
512
+ (panelDataSource instanceof SARViewer) ?
513
+ getSelectionBitset(panelDataSource.mutationCliffsSelection,
514
+ mutationCliffsToMaskInfo(panelDataSource.mutationCliffs ?? new Map(), trueModel.df.rowCount)) :
515
+ null;
516
+ if (bitset !== null) {
517
+ combinedBitset.or(bitset);
518
+ }
519
+
520
+
521
+ if (panelDataSource instanceof MonomerPosition) {
522
+ const invariantMapSelectionBitset = getSelectionBitset(panelDataSource.invariantMapSelection,
523
+ panelDataSource.monomerPositionStats);
524
+ if (invariantMapSelectionBitset !== null) {
525
+ combinedBitset.or(invariantMapSelectionBitset);
526
+ }
527
+ }
459
528
  }
460
- const table = trueModel.df.filter.anyFalse ? trueModel.df.clone(trueModel.df.filter, null, true) : trueModel.df;
461
- acc.addPane('Mutation Cliffs pairs', () => mutationCliffsWidget(trueModel.df, trueModel).root);
462
- acc.addPane('Distribution', () => getDistributionWidget(table, trueModel).root);
463
- acc.addPane('Selection', () => getSelectionWidget(trueModel.df, trueModel));
464
529
 
530
+ const sarViewer = requestSource as any as SARViewer | LogoSummaryTable;
531
+ if (requestSource !== trueModel.settings && !(sarViewer instanceof LogoSummaryTable) &&
532
+ sarViewer.mutationCliffs != null) {
533
+ // MC and Selection are left
534
+ acc.addPane('Mutation Cliffs pairs', () => mutationCliffsWidget(trueModel.df, {
535
+ mutationCliffs: sarViewer.mutationCliffs!,
536
+ mutationCliffsSelection: sarViewer.mutationCliffsSelection,
537
+ gridColumns: trueModel.analysisView.grid.columns,
538
+ sequenceColumnName: sarViewer.sequenceColumnName,
539
+ positionColumns: sarViewer.positionColumns,
540
+ activityCol: sarViewer.getScaledActivityColumn(),
541
+ }).root, true);
542
+ }
543
+ const isModelSource = requestSource === trueModel.settings;
544
+ const totalMonomerPositionSelection = isModelSource ? this.webLogoSelection :
545
+ (requestSource instanceof MonomerPosition) ? requestSource.invariantMapSelection : {};
546
+ const clusterSelection = (requestSource instanceof LogoSummaryTable) ? requestSource.clusterSelection :
547
+ trueLSTViewer?.clusterSelection ?? {};
548
+ acc.addPane('Distribution', () => getDistributionWidget(trueModel.df, {
549
+ peptideSelection: combinedBitset,
550
+ columns: isModelSource ? trueModel.settings!.columns ?? {} :
551
+ (requestSource as PeptideViewer).getAggregationColumns(),
552
+ activityCol: isModelSource ? trueModel.getScaledActivityColumn()! :
553
+ (requestSource as PeptideViewer).getScaledActivityColumn(),
554
+ monomerPositionSelection: totalMonomerPositionSelection,
555
+ clusterSelection: clusterSelection,
556
+ clusterColName: trueLSTViewer?.clustersColumnName,
557
+ }), true);
558
+ const areObjectsEqual = (o1?: AggregationColumns | null, o2?: AggregationColumns | null): boolean => {
559
+ if (o1 == null || o2 == null) {
560
+ return false;
561
+ }
562
+
563
+
564
+ for (const [key, value] of Object.entries(o1)) {
565
+ if (value !== o2[key]) {
566
+ return false;
567
+ }
568
+ }
569
+ return true;
570
+ };
571
+ acc.addPane('Selection', () => getSelectionWidget(trueModel.df, {
572
+ positionColumns: isModelSource ? trueModel.positionColumns! :
573
+ (requestSource as SARViewer | LogoSummaryTable).positionColumns,
574
+ columns: isModelSource ? trueModel.settings!.columns ?? {} :
575
+ (requestSource as SARViewer | LogoSummaryTable).getAggregationColumns(),
576
+ activityColumn: isModelSource ? trueModel.getScaledActivityColumn()! :
577
+ (requestSource as SARViewer | LogoSummaryTable).getScaledActivityColumn(),
578
+ gridColumns: trueModel.analysisView.grid.columns,
579
+ colorPalette: pickUpPalette(trueModel.df.getCol(isModelSource ? trueModel.settings!.sequenceColumnName :
580
+ (requestSource as SARViewer | LogoSummaryTable).sequenceColumnName)),
581
+ tableSelection: trueModel.getCombinedSelection(),
582
+ isAnalysis: trueModel.settings !== null && (isModelSource ||
583
+ areObjectsEqual(trueModel.settings.columns, (requestSource as PeptideViewer).getAggregationColumns())),
584
+ }), true);
465
585
  return acc;
466
586
  }
467
587
 
588
+ /**
589
+ * @param [isFiltered] - Whether to return filtered activity column
590
+ * @return - Scaled activity column
591
+ */
592
+ getScaledActivityColumn(isFiltered: boolean = false): DG.Column<number> | null {
593
+ const scaledActivityColumn = this.df.col(C.COLUMNS_NAMES.ACTIVITY);
594
+ if (isFiltered && scaledActivityColumn !== null) {
595
+ return DG.DataFrame.fromColumns([scaledActivityColumn]).clone(this.df.filter)
596
+ .getCol(scaledActivityColumn.name) as DG.Column<number>;
597
+ }
598
+ return scaledActivityColumn as DG.Column<number> | null;
599
+ }
600
+
601
+ /**
602
+ * Sets grid properties such as column semtypes, visibility, order, width, renderers and tooltips
603
+ */
468
604
  updateGrid(): void {
469
605
  this.joinDataFrames();
470
606
  this.createScaledCol();
471
- // this.setWebLogoInteraction();
472
607
  this.webLogoBounds = {};
473
608
 
474
- CR.setWebLogoRenderer(this.analysisView.grid, this);
609
+ const cellRendererOptions: CR.WebLogoCellRendererOptions = {
610
+ selectionCallback: (monomerPosition: type.SelectionItem, options: type.SelectionOptions): void =>
611
+ this.modifyWebLogoSelection(monomerPosition, options),
612
+ unhighlightCallback: (): void => this.unhighlight(),
613
+ colorPalette: () => pickUpPalette(this.df.getCol(this.settings!.sequenceColumnName)),
614
+ webLogoBounds: () => this.webLogoBounds,
615
+ cachedWebLogoTooltip: () => this.cachedWebLogoTooltip,
616
+ highlightCallback: (mp: type.SelectionItem, df: DG.DataFrame, mpStats: MonomerPositionStats): void =>
617
+ highlightMonomerPosition(mp, df, mpStats),
618
+ isSelectionTable: false,
619
+ headerSelectedMonomers: () => this.webLogoSelectedMonomers,
620
+ };
621
+ if (this.monomerPositionStats === null || this.positionColumns === null) {
622
+ throw new Error('PeptidesError: Could not updage grid: monomerPositionStats or positionColumns are null');
623
+ }
624
+
625
+
626
+ CR.setWebLogoRenderer(this.analysisView.grid, this.monomerPositionStats, this.positionColumns,
627
+ this.getScaledActivityColumn()!, cellRendererOptions);
475
628
  if (!this._layoutEventInitialized) {
476
629
  grok.events.onViewLayoutApplied.subscribe((layout) => {
477
- if (layout.view.id === this.analysisView.id)
630
+ if (layout.view.id === this.analysisView.id) {
478
631
  this.updateGrid();
632
+ }
479
633
  });
480
634
  this._layoutEventInitialized = true;
481
635
  }
@@ -485,45 +639,22 @@ export class PeptidesModel {
485
639
  this.setGridProperties();
486
640
  }
487
641
 
488
- initInvariantMapSelection(options: {notify?: boolean} = {}): void {
489
- options.notify ??= true;
490
-
491
- const tempSelection: type.Selection = {};
492
- const positionColumns = this.positionColumns.toArray().map((col) => col.name);
493
- for (const pos of positionColumns)
494
- tempSelection[pos] = [];
495
-
496
- if (options.notify)
497
- this.invariantMapSelection = tempSelection;
498
- else
499
- this._invariantMapSelection = tempSelection;
500
- }
501
-
502
- initMutationCliffsSelection(options: {notify?: boolean} = {}): void {
503
- options.notify ??= true;
504
-
505
- const tempSelection: type.Selection = {};
506
- const positionColumns = this.positionColumns.toArray().map((col) => col.name);
507
- for (const pos of positionColumns)
508
- tempSelection[pos] = [];
509
-
510
- if (options.notify)
511
- this.mutationCliffsSelection = tempSelection;
512
- else
513
- this._mutationCliffsSelection = tempSelection;
514
- }
515
-
642
+ /**
643
+ * Splits sequences and adds position columns to this.df.
644
+ */
516
645
  joinDataFrames(): void {
517
646
  // append splitSeqDf columns to source table and make sure columns are not added more than once
518
647
  const name = this.df.name;
519
648
  const cols = this.df.columns;
520
- const splitSeqDf = this.buildSplitSeqDf();
649
+ const splitSeqDf = splitAlignedSequences(this.df.getCol(this.settings!.sequenceColumnName));
521
650
  const positionColumns = splitSeqDf.columns.names();
522
651
  for (const colName of positionColumns) {
523
652
  let col = this.df.col(colName);
524
653
  const newCol = splitSeqDf.getCol(colName);
525
- if (col !== null)
654
+ if (col !== null) {
526
655
  cols.remove(colName);
656
+ }
657
+
527
658
 
528
659
  const newColCat = newCol.categories;
529
660
  const newColData = newCol.getRawData();
@@ -535,143 +666,140 @@ export class PeptidesModel {
535
666
  this.df.name = name;
536
667
  }
537
668
 
669
+ /**
670
+ * Creates scaled activity column
671
+ */
538
672
  createScaledCol(): void {
539
673
  const sourceGrid = this.analysisView.grid;
540
- const scaledCol = scaleActivity(this.df.getCol(this.settings.activityColumnName!), this.settings.scaling);
674
+ const scaledCol = scaleActivity(this.df.getCol(this.settings!.activityColumnName),
675
+ this.settings!.activityScaling);
541
676
  //TODO: make another func
542
- this.df.columns.replace(C.COLUMNS_NAMES.ACTIVITY_SCALED, scaledCol);
677
+ this.df.columns.replace(COLUMNS_NAMES.ACTIVITY, scaledCol);
543
678
 
544
679
  sourceGrid.columns.setOrder([scaledCol.name]);
545
680
  }
546
681
 
547
- initClusterSelection(options: {notify?: boolean} = {}): void {
548
- options.notify ??= true;
549
-
550
- const newClusterSelection = {} as type.Selection;
551
- newClusterSelection[CLUSTER_TYPE.ORIGINAL] = [];
552
- newClusterSelection[CLUSTER_TYPE.CUSTOM] = [];
553
- if (options.notify)
554
- this.clusterSelection = newClusterSelection;
555
- else
556
- this._clusterSelection = newClusterSelection;
557
- }
558
-
559
- highlightMonomerPosition(monomerPosition: type.SelectionItem): void {
560
- const bitArray = new BitArray(this.df.rowCount);
561
- if (monomerPosition.positionOrClusterType === C.COLUMNS_NAMES.MONOMER) {
562
- const positionStats = Object.values(this.monomerPositionStats);
563
- for (const posStat of positionStats) {
564
- const monomerPositionStats = (posStat as PositionStats)[monomerPosition.monomerOrCluster];
565
- if (monomerPositionStats ?? false)
566
- bitArray.or(monomerPositionStats!.mask);
567
- }
568
- } else {
569
- const monomerPositionStats = this.monomerPositionStats[monomerPosition.positionOrClusterType]![monomerPosition.monomerOrCluster];
570
- if (monomerPositionStats ?? false)
571
- bitArray.or(monomerPositionStats!.mask);
682
+ /**
683
+ * Resets rows highlighting
684
+ */
685
+ unhighlight(): void {
686
+ if (!this.isHighlighting) {
687
+ return;
572
688
  }
573
689
 
574
- this.df.rows.highlight((i) => bitArray.getBit(i));
575
- this.isHighlighting = true;
576
- }
577
-
578
- highlightCluster(cluster: type.SelectionItem): void {
579
- const bitArray = this.clusterStats[cluster.positionOrClusterType as ClusterType][cluster.monomerOrCluster].mask;
580
- this.df.rows.highlight((i) => bitArray.getBit(i));
581
- this.isHighlighting = true;
582
- }
583
690
 
584
- unhighlight(): void {
585
- if (!this.isHighlighting)
586
- return;
587
691
  this.df.rows.highlight(null);
588
692
  this.isHighlighting = false;
589
693
  }
590
694
 
695
+ /**
696
+ * Sets tooltips to analysis grid
697
+ */
591
698
  setTooltips(): void {
592
699
  this.analysisView.grid.onCellTooltip((cell, x, y) => {
593
- if (cell.isColHeader && cell.tableColumn!.semType === C.SEM_TYPES.MONOMER)
700
+ if (cell.isColHeader && cell.tableColumn?.semType === C.SEM_TYPES.MONOMER) {
594
701
  return true;
595
- if (!(cell.isTableCell && cell.tableColumn!.semType === C.SEM_TYPES.MONOMER))
702
+ }
703
+
704
+
705
+ if (!(cell.isTableCell && cell.tableColumn?.semType === C.SEM_TYPES.MONOMER)) {
596
706
  return false;
707
+ }
708
+
597
709
 
598
710
  showMonomerTooltip(cell.cell.value, x, y);
599
711
  return true;
600
712
  });
601
713
  }
602
714
 
603
- setBitsetCallback(): void {
604
- if (this.isBitsetChangedInitialized)
605
- return;
606
- const selection = this.df.selection;
607
- const filter = this.df.filter;
608
-
609
- const getCombinedSelection = (): DG.BitSet => {
610
- const combinedSelection = new BitArray(this.df.rowCount, false);
611
- // Invariant map selection
612
- for (const [position, monomerList] of Object.entries(this.invariantMapSelection)) {
715
+ /**
716
+ * Builds total analysis selection that comes from viewers and components
717
+ * @return - Total analysis selection
718
+ */
719
+ getCombinedSelection(): DG.BitSet {
720
+ const combinedSelection = new BitArray(this.df.rowCount, false);
721
+ // Invariant map selection
722
+ const addInvariantMapSelection = (selection: type.Selection, stats: MonomerPositionStats | null): void => {
723
+ for (const [position, monomerList] of Object.entries(selection)) {
613
724
  for (const monomer of monomerList) {
614
- const monomerPositionStats = this.monomerPositionStats[position]![monomer]!;
725
+ const positionStats = stats?.[position];
726
+ if (typeof positionStats === 'undefined') {
727
+ continue;
728
+ }
729
+
730
+
731
+ const monomerPositionStats = positionStats[monomer];
732
+ if (typeof monomerPositionStats === 'undefined') {
733
+ continue;
734
+ }
735
+
736
+
615
737
  combinedSelection.or(monomerPositionStats.mask);
616
738
  }
617
739
  }
740
+ };
618
741
 
619
- // Mutation cliffs selection
620
- for (const [position, monomerList] of Object.entries(this.mutationCliffsSelection)) {
742
+ addInvariantMapSelection(this.webLogoSelection, this.monomerPositionStats);
743
+ const mpViewer = this.findViewer(VIEWER_TYPE.MONOMER_POSITION) as MonomerPosition | null;
744
+ addInvariantMapSelection(mpViewer?.invariantMapSelection ?? {}, mpViewer?.monomerPositionStats ?? null);
745
+
746
+ // Mutation cliffs selection
747
+ const addMutationCliffsSelection = (selection: type.Selection, mc: type.MutationCliffs | null): void => {
748
+ for (const [position, monomerList] of Object.entries(selection)) {
621
749
  for (const monomer of monomerList) {
622
- const substitutions = this.mutationCliffs?.get(monomer)?.get(position) ?? null;
623
- if (substitutions === null)
750
+ const substitutions = mc?.get(monomer)?.get(position) ?? null;
751
+ if (substitutions === null) {
624
752
  continue;
753
+ }
754
+
755
+
625
756
  for (const [key, value] of substitutions.entries()) {
626
757
  combinedSelection.setTrue(key);
627
- for (const v of value)
758
+ for (const v of value) {
628
759
  combinedSelection.setTrue(v);
760
+ }
629
761
  }
630
762
  }
631
763
  }
764
+ };
765
+ addMutationCliffsSelection(mpViewer?.mutationCliffsSelection ?? {}, mpViewer?.mutationCliffs ?? null);
632
766
 
633
- // Cluster selection
634
- for (const clustType of Object.keys(this.clusterSelection)) {
635
- for (const clust of this.clusterSelection[clustType]) {
636
- const clusterStats = this.clusterStats[clustType as CLUSTER_TYPE][clust]!;
637
- combinedSelection.or(clusterStats.mask);
638
- }
767
+ const mprViewer = this.findViewer(VIEWER_TYPE.MOST_POTENT_RESIDUES) as MostPotentResidues | null;
768
+ addMutationCliffsSelection(mprViewer?.mutationCliffsSelection ?? {}, mprViewer?.mutationCliffs ?? null);
769
+
770
+ // Cluster selection
771
+ const lstViewer = this.findViewer(VIEWER_TYPE.LOGO_SUMMARY_TABLE) as LogoSummaryTable | null;
772
+ for (const clustType of Object.keys(lstViewer?.clusterSelection ?? {})) {
773
+ for (const clust of lstViewer!.clusterSelection[clustType] ?? []) {
774
+ const clusterStats = lstViewer!.clusterStats[clustType as CLUSTER_TYPE][clust];
775
+ combinedSelection.or(clusterStats.mask);
639
776
  }
777
+ }
640
778
 
641
- return DG.BitSet.fromBytes(combinedSelection.buffer.buffer, combinedSelection.length);
642
- };
779
+ return DG.BitSet.fromBytes(combinedSelection.buffer.buffer, combinedSelection.length);
780
+ }
781
+
782
+
783
+ /**
784
+ * Sets selection and filter changed callbacks
785
+ */
786
+ setBitsetCallback(): void {
787
+ if (this.isBitsetChangedInitialized) {
788
+ return;
789
+ }
643
790
 
644
- const getLatestSelection = (): DG.BitSet => {
645
- if (this.latestSelectionItem === null)
646
- return getCombinedSelection();
647
- if (this.latestSelectionItem.kind === SELECTION_MODE.INVARIANT_MAP) {
648
- const monomerPositionStats = this.monomerPositionStats[this.latestSelectionItem.positionOrClusterType]![this.latestSelectionItem.monomerOrCluster]!;
649
- return DG.BitSet.fromBytes(monomerPositionStats.mask.buffer.buffer, monomerPositionStats.mask.length);
650
- } else if (this.latestSelectionItem.kind === SELECTION_MODE.MUTATION_CLIFFS) {
651
- const substitutions = this.mutationCliffs?.get(this.latestSelectionItem.monomerOrCluster)?.get(this.latestSelectionItem.positionOrClusterType) ?? null;
652
- if (substitutions === null)
653
- throw new Error(`Couldn't find substitutions for ${this.latestSelectionItem.monomerOrCluster} at ${this.latestSelectionItem.positionOrClusterType}`);
654
- const latestSelection = new BitArray(this.df.rowCount, false);
655
- for (const [key, value] of substitutions.entries()) {
656
- latestSelection.setTrue(key);
657
- for (const v of value)
658
- latestSelection.setTrue(v);
659
- }
660
- return DG.BitSet.fromBytes(latestSelection.buffer.buffer, latestSelection.length);
661
- } else if (this.latestSelectionItem.kind === 'Cluster') {
662
- const clusterStats = this.clusterStats[this.latestSelectionItem.positionOrClusterType as CLUSTER_TYPE][this.latestSelectionItem.monomerOrCluster]!;
663
- return DG.BitSet.fromBytes(clusterStats.mask.buffer.buffer, clusterStats.mask.length);
664
- }
665
- throw new Error(`Unknown selection kind: ${this.latestSelectionItem.kind}`);
666
- };
791
+
792
+ const selection = this.df.selection;
793
+ const filter = this.df.filter;
667
794
 
668
795
  const showAccordion = (): void => {
669
796
  const acc = this.createAccordion();
670
- if (acc === null)
797
+ if (acc === null) {
671
798
  return;
799
+ }
800
+
801
+
672
802
  grok.shell.o = acc.root;
673
- for (const pane of acc.panes)
674
- pane.expanded = true;
675
803
  };
676
804
 
677
805
  selection.onChanged.subscribe(() => {
@@ -679,87 +807,163 @@ export class PeptidesModel {
679
807
  this.controlFire = false;
680
808
  return;
681
809
  }
682
- if (!this.isUserChangedSelection)
683
- selection.copyFrom(getLatestSelection(), false);
684
- showAccordion();
685
- this.isUserChangedSelection = true;
810
+ try {
811
+ if (!this.isUserChangedSelection) {
812
+ selection.copyFrom(this.getCombinedSelection(), false);
813
+ }
814
+ } catch (e) {
815
+ _package.logger.debug('Peptides: Error on selection changed');
816
+ _package.logger.debug(e as string);
817
+ } finally {
818
+ showAccordion();
819
+ }
686
820
  });
687
821
 
688
822
  filter.onChanged.subscribe(() => {
689
- if (this.controlFire) {
690
- this.controlFire = false;
691
- return;
692
- }
693
- const lstViewer = this.findViewer(VIEWER_TYPE.LOGO_SUMMARY_TABLE) as LogoSummaryTable | null;
694
- if (lstViewer !== null && typeof lstViewer.model !== 'undefined') {
695
- lstViewer.createLogoSummaryTableGrid();
696
- lstViewer.render();
823
+ try {
824
+ if (this.controlFire) {
825
+ this.controlFire = false;
826
+ return;
827
+ }
828
+ const lstViewer = this.findViewer(VIEWER_TYPE.LOGO_SUMMARY_TABLE) as LogoSummaryTable | null;
829
+ if (lstViewer !== null && typeof lstViewer.model !== 'undefined') {
830
+ lstViewer.createLogoSummaryTableGrid();
831
+ lstViewer.render();
832
+ }
833
+ } catch (e) {
834
+ _package.logger.debug('Peptides: Error on filter changed');
835
+ _package.logger.debug(e as string);
836
+ } finally {
837
+ showAccordion();
697
838
  }
698
- showAccordion();
699
839
  });
700
840
 
701
841
  this.isBitsetChangedInitialized = true;
702
842
  }
703
843
 
704
- fireBitsetChanged(fireFilterChanged: boolean = false): void {
844
+ /**
845
+ * Fires bitset changed event and rebuilds accordion in context panel
846
+ * @param source - Source of bitset changed event
847
+ * @param fireFilterChanged - Whether to fire filter changed event
848
+ */
849
+ fireBitsetChanged(source: VIEWER_TYPE | null, fireFilterChanged: boolean = false): void {
850
+ this.accordionSource = source;
851
+ if (!this.isBitsetChangedInitialized) {
852
+ this.setBitsetCallback();
853
+ }
854
+
855
+
705
856
  this.isUserChangedSelection = false;
706
857
  this.df.selection.fireChanged();
707
- if (fireFilterChanged)
858
+ if (fireFilterChanged) {
708
859
  this.df.filter.fireChanged();
860
+ }
861
+
709
862
 
710
863
  // Fire bitset changed event again to update UI
711
864
  this.controlFire = true;
712
865
  this.df.selection.fireChanged();
713
- if (fireFilterChanged)
866
+ if (fireFilterChanged) {
714
867
  this.df.filter.fireChanged();
868
+ }
869
+
715
870
 
716
- this.headerSelectedMonomers = calculateSelected(this.df);
871
+ this.isUserChangedSelection = true;
872
+ this.webLogoSelectedMonomers = calculateSelected(this.df);
717
873
  }
718
874
 
875
+ /**
876
+ * Sets grid properties such
877
+ * @param props - Grid properties
878
+ */
719
879
  setGridProperties(props?: DG.IGridLookSettings): void {
720
880
  const sourceGrid = this.analysisView.grid;
721
881
  const sourceGridProps = sourceGrid.props;
722
882
  sourceGridProps.allowColSelection = props?.allowColSelection ?? false;
723
883
  sourceGridProps.allowEdit = props?.allowEdit ?? false;
884
+ sourceGridProps.showReadOnlyNotifications = props?.showReadOnlyNotifications ?? false;
724
885
  sourceGridProps.showCurrentRowIndicator = props?.showCurrentRowIndicator ?? false;
725
- this.df.temp[C.EMBEDDING_STATUS] = false;
726
- const positionCols = this.positionColumns.toArray();
886
+ const positionCols = this.positionColumns;
887
+ if (positionCols === null) {
888
+ throw new Error('PeptidesError: Could not set grid properties: positionColumns are null');
889
+ }
890
+
891
+
727
892
  let maxWidth = 10;
728
893
  const canvasContext = sourceGrid.canvas.getContext('2d');
894
+ if (canvasContext === null) {
895
+ throw new Error('PeptidesError: Could not set grid properties: canvas context is null');
896
+ }
897
+
898
+
729
899
  for (const positionCol of positionCols) {
730
900
  // Longest category
731
901
  const maxCategory = monomerToShort(positionCol.categories.reduce((a, b) => a.length > b.length ? a : b), 6);
732
902
  // Measure text width of longest category
733
- const width = Math.ceil(canvasContext!.measureText(maxCategory).width);
903
+ const width = Math.ceil(canvasContext.measureText(maxCategory).width);
734
904
  maxWidth = Math.max(maxWidth, width);
735
905
  }
906
+
907
+ const posCols = positionCols.map((col) => col.name);
908
+ for (let colIdx = 1; colIdx < this.analysisView.grid.columns.length; ++colIdx) {
909
+ const gridCol = this.analysisView.grid.columns.byIndex(colIdx);
910
+ if (gridCol === null) {
911
+ throw new Error(`PeptidesError: Could not get analysis view: grid column with index '${colIdx}' is null`);
912
+ } else if (gridCol.column === null) {
913
+ throw new Error(`PeptidesError: Could not get analysis view: grid column with index '${colIdx}' has null ` +
914
+ `column`);
915
+ }
916
+ gridCol.visible = posCols.includes(gridCol.column.name) || (gridCol.column.name === C.COLUMNS_NAMES.ACTIVITY);
917
+ }
918
+
736
919
  setTimeout(() => {
737
- for (const positionCol of positionCols)
738
- sourceGrid.col(positionCol.name)!.width = maxWidth + 15;
920
+ for (const positionCol of positionCols) {
921
+ const gridCol = sourceGrid.col(positionCol.name);
922
+ if (gridCol === null) {
923
+ throw new Error(`PeptidesError: Could not set column width: grid column '${positionCol.name}' is null`);
924
+ }
925
+
926
+
927
+ gridCol.width = maxWidth + 15;
928
+ }
739
929
  }, 100);
740
930
  }
741
931
 
932
+ /**
933
+ * Closes peptides viewer
934
+ * @param viewerType - Viewer type to close
935
+ */
742
936
  closeViewer(viewerType: VIEWER_TYPE): void {
743
937
  const viewer = this.findViewer(viewerType);
744
938
  viewer?.detach();
745
939
  viewer?.close();
746
940
  }
747
941
 
942
+ /**
943
+ * Finds viewer node in the analysis view
944
+ * @param viewerType - Viewer type to find
945
+ * @return - Viewer node or null if not found
946
+ */
748
947
  findViewerNode(viewerType: VIEWER_TYPE): DG.DockNode | null {
749
948
  for (const node of this.analysisView.dockManager.rootNode.children) {
750
- if (node.container.containerElement.innerHTML.includes(viewerType))
949
+ if (node.container.containerElement.innerHTML.includes(viewerType)) {
751
950
  return node;
951
+ }
752
952
  }
753
953
  return null;
754
954
  }
755
955
 
956
+ /**
957
+ * Adds Dendrogram viewer to the analysis view
958
+ * @returns
959
+ */
756
960
  async addDendrogram(): Promise<void> {
757
961
  const pi = DG.TaskBarProgressIndicator.create('Calculating distance matrix...');
758
962
  try {
759
- const pepColValues: string[] = this.df.getCol(this.settings.sequenceColumnName!).toList();
963
+ const pepColValues: string[] = this.df.getCol(this.settings!.sequenceColumnName).toList();
760
964
  this._dm ??= new DistanceMatrix(await createDistanceMatrixWorker(pepColValues, StringMetricsNames.Levenshtein));
761
965
  const leafCol = this.df.col('~leaf-id') ?? this.df.columns.addNewString('~leaf-id').init((i) => i.toString());
762
- const treeHelper: ITreeHelper = getTreeHelperInstance()!;
966
+ const treeHelper: ITreeHelper = getTreeHelperInstance();
763
967
  const treeNode = await treeHelper.hierarchicalClusteringByDistance(this._dm, 'ward');
764
968
 
765
969
  this.df.setTag(treeTAGS.NEWICK, treeHelper.toNewick(treeNode));
@@ -775,10 +979,17 @@ export class PeptidesModel {
775
979
  }
776
980
  }
777
981
 
778
- /** Class initializer */
779
- init(): void {
780
- if (this.isInitialized)
982
+ /**
983
+ * Analysis initializer
984
+ * @param settings - Analysis settings
985
+ */
986
+ init(settings: type.PeptidesSettings): void {
987
+ if (this.isInitialized) {
781
988
  return;
989
+ }
990
+
991
+
992
+ this.settings = settings;
782
993
  this.isInitialized = true;
783
994
 
784
995
  if (!this.isRibbonSet && this.df.getTag(C.TAGS.MULTIPLE_VIEWS) !== '1') {
@@ -790,88 +1001,131 @@ export class PeptidesModel {
790
1001
  }
791
1002
 
792
1003
  this.subs.push(grok.events.onAccordionConstructed.subscribe((acc) => {
793
- if (!(grok.shell.o instanceof DG.SemanticValue || (grok.shell.o instanceof DG.Column && this.df.columns.toList().includes(grok.shell.o))))
1004
+ if (!(grok.shell.o instanceof DG.SemanticValue || (grok.shell.o instanceof DG.Column &&
1005
+ this.df.columns.toList().includes(grok.shell.o)))) {
794
1006
  return;
1007
+ }
795
1008
 
796
- const actionsPane = acc.getPane('Actions');
797
1009
 
1010
+ const actionsPane = acc.getPane('Actions');
798
1011
  const actionsHost = $(actionsPane.root).find('.d4-flex-col');
799
1012
  const calculateIdentity = ui.label('Calculate identity');
800
1013
  calculateIdentity.classList.add('d4-link-action');
801
- ui.tooltip.bind(calculateIdentity, 'Adds a column with fractions of matching monomers against sequence in the current row');
1014
+ ui.tooltip.bind(calculateIdentity,
1015
+ 'Adds a column with fractions of matching monomers against sequence in the current row');
802
1016
  calculateIdentity.onclick = (): void => {
803
- const seqCol = this.df.getCol(this.settings.sequenceColumnName!);
804
- calculateScores(this.df, seqCol, seqCol.get(this.df.currentRowIdx), SCORE.IDENTITY);
1017
+ const seqCol = this.df.getCol(this.settings!.sequenceColumnName);
1018
+ calculateScores(this.df, seqCol, seqCol.get(this.df.currentRowIdx), SCORE.IDENTITY)
1019
+ .then((col: DG.Column<number>) => col.setTag(C.TAGS.IDENTITY_TEMPLATE, seqCol.get(this.df.currentRowIdx)))
1020
+ .catch((e) => _package.logger.debug(e));
805
1021
  };
806
1022
  actionsHost.append(ui.span([calculateIdentity], 'd4-markdown-row'));
807
1023
 
808
1024
  const calculateSimilarity = ui.label('Calculate similarity');
809
1025
  calculateSimilarity.classList.add('d4-link-action');
810
- ui.tooltip.bind(calculateSimilarity, 'Adds a column with sequence similarity scores against sequence in the current row');
1026
+ ui.tooltip.bind(calculateSimilarity,
1027
+ 'Adds a column with sequence similarity scores against sequence in the current row');
811
1028
  calculateSimilarity.onclick = (): void => {
812
- const seqCol = this.df.getCol(this.settings.sequenceColumnName!);
813
- calculateScores(this.df, seqCol, seqCol.get(this.df.currentRowIdx), SCORE.SIMILARITY);
1029
+ const seqCol = this.df.getCol(this.settings!.sequenceColumnName);
1030
+ calculateScores(this.df, seqCol, seqCol.get(this.df.currentRowIdx), SCORE.SIMILARITY)
1031
+ .then((col: DG.Column<number>) => col.setTag(C.TAGS.SIMILARITY_TEMPLATE, seqCol.get(this.df.currentRowIdx)))
1032
+ .catch((e) => _package.logger.debug(e));
814
1033
  };
815
1034
  actionsHost.append(ui.span([calculateSimilarity], 'd4-markdown-row'));
816
1035
  }));
817
1036
 
818
1037
  this.subs.push(grok.events.onViewRemoved.subscribe((view) => {
819
- if (view.id === this.analysisView.id)
1038
+ if (view.id === this.analysisView.id) {
820
1039
  this.subs.forEach((v) => v.unsubscribe());
1040
+ }
1041
+
1042
+
821
1043
  grok.log.debug(`Peptides: view ${view.name} removed`);
822
1044
  }));
823
1045
  this.subs.push(grok.events.onTableRemoved.subscribe((table: DG.DataFrame) => {
824
- if (table.id === this.df.id)
1046
+ if (table.id === this.df.id) {
825
1047
  this.subs.forEach((v) => v.unsubscribe());
1048
+ }
1049
+
1050
+
826
1051
  grok.log.debug(`Peptides: table ${table.name} removed`);
827
1052
  }));
828
1053
  this.subs.push(grok.events.onProjectClosed.subscribe((project: DG.Project) => {
829
- if (project.id === grok.shell.project.id)
1054
+ if (project.id === grok.shell.project.id) {
830
1055
  this.subs.forEach((v) => v.unsubscribe());
1056
+ }
1057
+
1058
+
831
1059
  grok.log.debug(`Peptides: project ${project.name} closed`);
832
1060
  }));
833
1061
 
834
- this.fireBitsetChanged(true);
835
- if (typeof this.settings.targetColumnName === 'undefined')
836
- this.updateMutationCliffs();
1062
+ this.fireBitsetChanged(null, true);
837
1063
 
838
1064
  this.analysisView.grid.invalidate();
839
1065
  }
840
1066
 
1067
+ /**
1068
+ * Finds viewer by type
1069
+ * @param viewerType - Viewer type to find
1070
+ * @return - Viewer or null if not found
1071
+ */
841
1072
  findViewer(viewerType: VIEWER_TYPE): DG.Viewer | null {
842
1073
  return wu(this.analysisView.viewers).find((v) => v.type === viewerType) || null;
843
1074
  }
844
1075
 
845
- async addLogoSummaryTable(): Promise<void> {
846
- this.closeViewer(VIEWER_TYPE.MONOMER_POSITION);
847
- this.closeViewer(VIEWER_TYPE.MOST_POTENT_RESIDUES);
848
- const logoSummaryTable = await this.df.plot.fromType(VIEWER_TYPE.LOGO_SUMMARY_TABLE) as LogoSummaryTable;
1076
+ /**
1077
+ * Adds Logo Summary Table viewer to the analysis view
1078
+ * @param [viewerProperties] - Viewer properties
1079
+ */
1080
+ async addLogoSummaryTable(viewerProperties?: ILogoSummaryTable): Promise<void> {
1081
+ viewerProperties ??= {
1082
+ sequenceColumnName: this.settings!.sequenceColumnName,
1083
+ clustersColumnName: wu(this.df.columns.categorical).next().value,
1084
+ activityColumnName: this.settings!.activityColumnName,
1085
+ activityScaling: this.settings!.activityScaling,
1086
+ };
1087
+ const logoSummaryTable = await this.df.plot
1088
+ .fromType(VIEWER_TYPE.LOGO_SUMMARY_TABLE, viewerProperties) as LogoSummaryTable;
849
1089
  this.analysisView.dockManager.dock(logoSummaryTable, DG.DOCK_TYPE.RIGHT, null, VIEWER_TYPE.LOGO_SUMMARY_TABLE);
850
- if (this.settings.showMonomerPosition)
851
- await this.addMonomerPosition();
852
- if (this.settings.showMostPotentResidues)
853
- await this.addMostPotentResidues();
1090
+
854
1091
  logoSummaryTable.viewerGrid.invalidate();
855
1092
  }
856
1093
 
857
- async addMonomerPosition(): Promise<void> {
858
- const monomerPosition = await this.df.plot.fromType(VIEWER_TYPE.MONOMER_POSITION) as MonomerPosition;
1094
+ /**
1095
+ * Adds Monomer-Position viewer to the analysis view
1096
+ * @param [viewerProperties] - Viewer properties
1097
+ */
1098
+ async addMonomerPosition(viewerProperties?: ISARViewer): Promise<void> {
1099
+ viewerProperties ??= {
1100
+ maxMutations: 1,
1101
+ activityScaling: this.settings!.activityScaling,
1102
+ activityColumnName: this.settings!.activityColumnName,
1103
+ sequenceColumnName: this.settings!.sequenceColumnName,
1104
+ minActivityDelta: 0,
1105
+ };
1106
+ const monomerPosition = await this.df.plot
1107
+ .fromType(VIEWER_TYPE.MONOMER_POSITION, viewerProperties) as MonomerPosition;
859
1108
  const mostPotentResidues = this.findViewer(VIEWER_TYPE.MOST_POTENT_RESIDUES) as MostPotentResidues | null;
860
1109
  const dm = this.analysisView.dockManager;
861
1110
  const [dockType, refNode, ratio] = mostPotentResidues === null ? [DG.DOCK_TYPE.DOWN, null, undefined] :
862
1111
  [DG.DOCK_TYPE.LEFT, this.findViewerNode(VIEWER_TYPE.MOST_POTENT_RESIDUES), 0.7];
863
1112
  dm.dock(monomerPosition, dockType, refNode, VIEWER_TYPE.MONOMER_POSITION, ratio);
864
- if (typeof this.settings.targetColumnName !== 'undefined') {
865
- const target = monomerPosition.getProperty(MONOMER_POSITION_PROPERTIES.TARGET)!;
866
- const choices = this.df.getCol(this.settings.targetColumnName!).categories;
867
- target.choices = choices;
868
- target.set(monomerPosition, choices[0]);
869
- }
870
1113
  }
871
1114
 
872
- async addMostPotentResidues(): Promise<void> {
1115
+ /**
1116
+ * Adds Most Potent Residues viewer to the analysis view
1117
+ * @param [viewerProperties] - Viewer properties
1118
+ */
1119
+ async addMostPotentResidues(viewerProperties?: ISARViewer): Promise<void> {
1120
+ viewerProperties ??= {
1121
+ activityScaling: this.settings!.activityScaling,
1122
+ activityColumnName: this.settings!.activityColumnName,
1123
+ sequenceColumnName: this.settings!.sequenceColumnName,
1124
+ minActivityDelta: 0,
1125
+ maxMutations: 1,
1126
+ };
873
1127
  const mostPotentResidues =
874
- await this.df.plot.fromType(VIEWER_TYPE.MOST_POTENT_RESIDUES) as MostPotentResidues;
1128
+ await this.df.plot.fromType(VIEWER_TYPE.MOST_POTENT_RESIDUES, viewerProperties) as MostPotentResidues;
875
1129
  const monomerPosition = this.findViewer(VIEWER_TYPE.MONOMER_POSITION) as MonomerPosition | null;
876
1130
  const dm = this.analysisView.dockManager;
877
1131
  const [dockType, refNode, ratio] = monomerPosition === null ? [DG.DOCK_TYPE.DOWN, null, undefined] :
@@ -879,89 +1133,132 @@ export class PeptidesModel {
879
1133
  dm.dock(mostPotentResidues, dockType, refNode, VIEWER_TYPE.MOST_POTENT_RESIDUES, ratio);
880
1134
  }
881
1135
 
882
- addNewCluster(clusterName: string): void {
883
- const newClusterCol = DG.Column.fromBitSet(clusterName, this.getCompoundBitset());
884
- newClusterCol.setTag(C.TAGS.CUSTOM_CLUSTER, '1');
885
- newClusterCol.setTag(C.TAGS.ANALYSIS_COL, `${true}`);
886
- this.df.columns.add(newClusterCol);
887
- this.analysisView.grid.col(newClusterCol.name)!.visible = false;
888
- }
889
-
1136
+ /**
1137
+ * Creates new view from analysis dataframe selection, and adds LogoSummaryTable to it
1138
+ * @return - New view id
1139
+ */
890
1140
  createNewView(): string {
891
- const rowMask = this.getCompoundBitset();
892
- const newDfId = uuid.v4();
893
-
1141
+ const rowMask = this.getVisibleSelection();
894
1142
  const newDf = this.df.clone(rowMask);
895
- for (const [tag, value] of newDf.tags)
1143
+ for (const [tag, value] of newDf.tags) {
896
1144
  newDf.setTag(tag, tag === C.TAGS.SETTINGS ? value : '');
1145
+ }
1146
+
897
1147
 
898
1148
  newDf.name = 'Peptides Multiple Views';
899
1149
  newDf.setTag(C.TAGS.MULTIPLE_VIEWS, '1');
900
- newDf.setTag(C.TAGS.UUID, newDfId);
901
1150
 
902
1151
  const view = grok.shell.addTableView(newDf);
903
- view.addViewer(VIEWER_TYPE.LOGO_SUMMARY_TABLE);
904
-
905
- return newDfId;
906
- }
907
-
908
- modifyInvariantMapSelection(monomerPosition: type.SelectionItem, options: type.SelectionOptions = {shiftPressed: false, ctrlPressed: false}, notify: boolean = true): void {
909
- if (notify)
910
- this.invariantMapSelection = this.modifySelection(this.invariantMapSelection, monomerPosition, options);
911
- else
912
- this._invariantMapSelection = this.modifySelection(this._invariantMapSelection, monomerPosition, options);
913
- }
914
-
915
- modifyMutationCliffsSelection(monomerPosition: type.SelectionItem, options: type.SelectionOptions = {shiftPressed: false, ctrlPressed: false}, notify: boolean = true): void {
916
- if (notify)
917
- this.mutationCliffsSelection = this.modifySelection(this.mutationCliffsSelection, monomerPosition, options);
918
- else
919
- this._mutationCliffsSelection = this.modifySelection(this._mutationCliffsSelection, monomerPosition, options);
920
- }
921
-
922
- modifyClusterSelection(cluster: type.SelectionItem, options: type.SelectionOptions = {shiftPressed: false, ctrlPressed: false}, notify: boolean = true): void {
923
- if (notify)
924
- this.clusterSelection = this.modifySelection(this.clusterSelection, cluster, options);
925
- else
926
- this._clusterSelection = this.modifySelection(this._clusterSelection, cluster, options);
927
- }
928
-
929
- modifySelection(selection: type.Selection, clusterOrMonomerPosition: type.SelectionItem, options: type.SelectionOptions): type.Selection {
930
- const monomerList = selection[clusterOrMonomerPosition.positionOrClusterType];
931
- const monomerIndex = monomerList.indexOf(clusterOrMonomerPosition.monomerOrCluster);
932
- if (options.shiftPressed && options.ctrlPressed) {
933
- if (monomerIndex !== -1)
934
- monomerList.splice(monomerIndex, 1);
935
- } else if (options.ctrlPressed) {
936
- if (monomerIndex === -1)
937
- monomerList.push(clusterOrMonomerPosition.monomerOrCluster);
938
- else
939
- monomerList.splice(monomerIndex, 1);
940
- } else if (options.shiftPressed) {
941
- if (monomerIndex === -1)
942
- monomerList.push(clusterOrMonomerPosition.monomerOrCluster);
943
- } else {
944
- const selectionKeys = Object.keys(selection);
945
- selection = {};
946
- for (const posOrClustType of selectionKeys) {
947
- selection[posOrClustType] = [];
948
- if (posOrClustType === clusterOrMonomerPosition.positionOrClusterType)
949
- selection[posOrClustType].push(clusterOrMonomerPosition.monomerOrCluster);
950
- }
1152
+ const lstViewer = this.findViewer(VIEWER_TYPE.LOGO_SUMMARY_TABLE) as LogoSummaryTable | null;
1153
+ if (lstViewer != null) {
1154
+ view.addViewer(VIEWER_TYPE.LOGO_SUMMARY_TABLE, {
1155
+ [`${LST_PROPERTIES.SEQUENCE}${COLUMN_NAME}`]: lstViewer.sequenceColumnName,
1156
+ [`${LST_PROPERTIES.ACTIVITY}${COLUMN_NAME}`]: lstViewer.activityColumnName,
1157
+ [LST_PROPERTIES.ACTIVITY_SCALING]: lstViewer.activityScaling,
1158
+ [LST_PROPERTIES.WEB_LOGO_MODE]: lstViewer.webLogoMode,
1159
+ [LST_PROPERTIES.MEMBERS_RATIO_THRESHOLD]: lstViewer.membersRatioThreshold,
1160
+ [`${LST_PROPERTIES.CLUSTERS}${COLUMN_NAME}`]: lstViewer.clustersColumnName,
1161
+ });
951
1162
  }
952
- return selection;
1163
+
1164
+ return newDf.getTag(DG.TAGS.ID)!;
953
1165
  }
954
1166
 
1167
+ /**
1168
+ * Adds Sequence Space viewer to the analysis view
1169
+ */
955
1170
  async addSequenceSpace(): Promise<void> {
956
- const seqSpaceParams: {table: DG.DataFrame, molecules: DG.Column, methodName: DimReductionMethods,
957
- similarityMetric: BitArrayMetrics | MmDistanceFunctionsNames, plotEmbeddings: boolean,
958
- sparseMatrixThreshold?: number, options?: (IUMAPOptions | ITSNEOptions) & Options} =
959
- {table: this.df, molecules: this.df.getCol(this.settings.sequenceColumnName!),
960
- methodName: DimReductionMethods.UMAP, similarityMetric: MmDistanceFunctionsNames.MONOMER_CHEMICAL_DISTANCE,
961
- plotEmbeddings: true, sparseMatrixThreshold: 0.3, options: {'bypassLargeDataWarning': true}};
962
- const seqSpaceViewer: DG.ScatterPlotViewer | undefined = await grok.functions.call('Bio:sequenceSpaceTopMenu', seqSpaceParams);
963
- if (!(seqSpaceViewer instanceof DG.ScatterPlotViewer))
1171
+ if (this._sequenceSpaceViewer !== null) {
1172
+ try {
1173
+ this._sequenceSpaceViewer?.detach();
1174
+ this._sequenceSpaceViewer?.close();
1175
+ } catch (_) {}
1176
+ }
1177
+ if (this._sequenceSpaceCols.length !== 0)
1178
+ this._sequenceSpaceCols.forEach((col) => this.df.columns.remove(col));
1179
+
1180
+ this._sequenceSpaceCols = [];
1181
+ let seqCol = this.df.getCol(this.settings!.sequenceColumnName!);
1182
+ const uh = UnitsHandler.getOrCreate(seqCol);
1183
+ const isHelm = uh.isHelm();
1184
+ if (isHelm) {
1185
+ try {
1186
+ grok.shell.warning('Column is in HELM notation. Sequences space will linearize sequences from position 0 ' +
1187
+ 'prior to analysis');
1188
+ const linearCol = uh.convert(NOTATION.SEPARATOR, '/');
1189
+ const newName = this.df.columns.getUnusedName(`Separator(${seqCol.name})`);
1190
+ linearCol.name = newName;
1191
+ this.df.columns.add(linearCol, true);
1192
+ this.analysisView.grid.col(newName)!.visible = false;
1193
+ seqCol = linearCol;
1194
+ } catch (e) {
1195
+ grok.shell.error('Error on converting HELM notation to linear notation');
1196
+ grok.shell.error(e as string);
1197
+ return;
1198
+ }
1199
+ }
1200
+ const seqSpaceSettings = this.settings?.sequenceSpaceParams ?? new type.SequenceSpaceParams();
1201
+ const seqSpaceParams: {
1202
+ table: DG.DataFrame,
1203
+ molecules: DG.Column,
1204
+ methodName: DimReductionMethods,
1205
+ similarityMetric: BitArrayMetrics | MmDistanceFunctionsNames,
1206
+ plotEmbeddings: boolean,
1207
+ sparseMatrixThreshold?: number,
1208
+ clusterEmbeddings?: boolean,
1209
+ options?: (IUMAPOptions | ITSNEOptions) & Options
1210
+ } =
1211
+ {
1212
+ table: this.df,
1213
+ molecules: seqCol,
1214
+ methodName: DimReductionMethods.UMAP,
1215
+ similarityMetric: seqSpaceSettings.distanceF,
1216
+ plotEmbeddings: true,
1217
+ sparseMatrixThreshold: 0.3,
1218
+ options: {'bypassLargeDataWarning': true, dbScanEpsilon: seqSpaceSettings.epsilon, dbScanMinPts: seqSpaceSettings.minPts,
1219
+ preprocessingFuncArgs: {gapOpen: seqSpaceSettings.gapOpen, gapExtend: seqSpaceSettings.gapExtend, fingerprintType: seqSpaceSettings.fingerprintType}},
1220
+ clusterEmbeddings: seqSpaceSettings.clusterEmbeddings,
1221
+ };
1222
+
1223
+ // Use counter to unsubscribe when 2 columns are hidden
1224
+ let counter = 0;
1225
+ const addedColCount = seqSpaceSettings.clusterEmbeddings ? 3 : 2;
1226
+ const columnAddedSub = this.df.onColumnsAdded.subscribe((colArgs: DG.ColumnsArgs) => {
1227
+ for (const col of colArgs.columns) {
1228
+ if (col.name.startsWith('Embed_') || ( seqSpaceSettings.clusterEmbeddings && col.name.toLowerCase().startsWith('cluster'))) {
1229
+ const gridCol = this.analysisView.grid.col(col.name);
1230
+ if (gridCol == null) {
1231
+ continue;
1232
+ }
1233
+ gridCol.visible = false;
1234
+ this._sequenceSpaceCols.push(col.name);
1235
+ counter++;
1236
+ }
1237
+ }
1238
+ if (counter === addedColCount) {
1239
+ columnAddedSub.unsubscribe();
1240
+ }
1241
+ });
1242
+
1243
+ const seqSpaceViewer: DG.ScatterPlotViewer | undefined =
1244
+ await grok.functions.call('Bio:sequenceSpaceTopMenu', seqSpaceParams);
1245
+ if (!(seqSpaceViewer instanceof DG.ScatterPlotViewer)) {
964
1246
  return;
965
- seqSpaceViewer.props.colorColumnName = C.COLUMNS_NAMES.ACTIVITY_SCALED;
1247
+ }
1248
+
1249
+ if (!seqSpaceSettings.clusterEmbeddings) { // color by activity if clusters are not automatically generated.
1250
+ seqSpaceViewer.props.colorColumnName = this.getScaledActivityColumn()!.name;
1251
+ }
1252
+ seqSpaceViewer.props.showXSelector = false;
1253
+ seqSpaceViewer.props.showYSelector = false;
1254
+ this._sequenceSpaceViewer = seqSpaceViewer;
1255
+ seqSpaceViewer.onContextMenu.subscribe((menu) => {
1256
+ try {
1257
+ menu.item('Modify Sequence space parameters', () => {
1258
+ getSettingsDialog(this);
1259
+ });
1260
+ } catch (e) {
1261
+ }
1262
+ });
966
1263
  }
967
1264
  }