@datagrok/peptides 1.17.0 → 1.17.2

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