@datagrok/peptides 1.26.1 → 1.27.0

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@datagrok/peptides",
3
3
  "friendlyName": "Peptides",
4
- "version": "1.26.1",
4
+ "version": "1.27.0",
5
5
  "author": {
6
6
  "name": "Davit Rizhinashvili",
7
7
  "email": "drizhinashvili@datagrok.ai"
package/src/model.ts CHANGED
@@ -65,6 +65,7 @@ export enum VIEWER_TYPE {
65
65
  DENDROGRAM = 'Dendrogram',
66
66
  CLUSTER_MAX_ACTIVITY = 'Active peptide selection',
67
67
  MCL = 'MCL',
68
+ SEQUENCE_MUTATION_CLIFFS = 'Sequence Mutation Cliffs',
68
69
  }
69
70
 
70
71
  export type CachedWebLogoTooltip = { bar: string, tooltip: HTMLDivElement | null };
@@ -621,6 +622,7 @@ export class PeptidesModel {
621
622
  sequenceColumnName: sarViewer.sequenceColumnName,
622
623
  positionColumns: sarViewer.positionColumns,
623
624
  activityCol: sarViewer.getScaledActivityColumn(),
625
+ mutationCliffStats: sarViewer.cliffStats,
624
626
  }).root, true);
625
627
  }
626
628
  const isModelSource = requestSource === trueModel.settings;
@@ -146,6 +146,7 @@ category('Widgets: Mutation cliffs', () => {
146
146
  sequenceColumnName: sarViewer.sequenceColumnName,
147
147
  positionColumns: sarViewer.positionColumns,
148
148
  activityCol: scaledActivityCol,
149
+ mutationCliffStats: sarViewer.cliffStats,
149
150
  });
150
151
  });
151
152
  });
@@ -63,19 +63,31 @@ export function calculateCliffsStatistics(
63
63
  const monomerSubMap = cliffs.get(monomer)!;
64
64
  for (const position of monomerSubMap.keys()) {
65
65
  const subMap = monomerSubMap.get(position)!;
66
- const mask = new BitArray(activityArray.length, false);
67
66
  if (subMap.size === 0)
68
67
  continue;
68
+ // create two masks, one filtering the activities to only mutation cliffs (with given monomer at given position and its substitutions)
69
+ // another one corresponding to only the given monomer at given position within the mutation cliffs
70
+ const filterMask = new BitArray(activityArray.length, false);
71
+ const maskMCWithMonomerAtPosition = new BitArray(activityArray.length, false);
69
72
  for (const index of subMap.keys()) {
70
- mask.setFast(index, true);
73
+ // set the filter mask to true for all sequences within the mutation cliff pairs
74
+ filterMask.setFast(index, true);
71
75
  const toIndexes = subMap.get(index)!;
72
- toIndexes.forEach((i) => mask.setFast(i, true));
76
+ toIndexes.forEach((i) => filterMask.setFast(i, true));
77
+ // set the mask for sequences with the given monomer at the given position within the mutation cliffs
78
+ maskMCWithMonomerAtPosition.setFast(index, true);
73
79
  }
74
- const stats = getStats(activityArray, mask);
80
+ const stats = getStats(activityArray, maskMCWithMonomerAtPosition, filterMask);
81
+ stats.mask = filterMask; // store the filter mask for later use in the viewer
75
82
  minDiff = Math.min(minDiff, stats.meanDifference);
76
83
  maxDiff = Math.max(maxDiff, stats.meanDifference);
77
84
  minCount = Math.min(minCount, stats.count);
78
85
  maxCount = Math.max(maxCount, stats.count);
86
+ // here, stats will show the following
87
+ // count - number of sequences with the given monomer at the given position within the mutation cliffs
88
+ // mask.trueCount - number of unique sequences within the mutation cliffs (with given monomer at given position and its substitutions)
89
+ // meanDifference - difference between mean activity of sequences with the given monomer at the given position within the mutation cliffs
90
+ // and mean activity of other sequences within the mutation cliffs (with given monomer at given position substitutions)
79
91
  monomerStatsMap.set(position, stats);
80
92
  }
81
93
  }
package/src/utils/misc.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-len */
1
2
  import * as ui from 'datagrok-api/ui';
2
3
  import * as DG from 'datagrok-api/dg';
3
4
  import * as grok from 'datagrok-api/grok';
@@ -111,13 +112,10 @@ export function getDistributionPanel(hist: DG.Viewer<DG.IHistogramSettings>, sta
111
112
  const splitCol = hist.dataFrame.getCol(C.COLUMNS_NAMES.SPLIT_COL);
112
113
  const labels = [];
113
114
  const categories = splitCol.categories as SPLIT_CATEGORY[];
114
- const rawData = splitCol.getRawData();
115
115
  for (let categoryIdx = 0; categoryIdx < categories.length; ++categoryIdx) {
116
- if (!Object.values(SPLIT_CATEGORY).includes(categories[categoryIdx]))
116
+ if (!categories[categoryIdx])
117
117
  continue;
118
-
119
-
120
- const color = DG.Color.toHtml(splitCol.meta.colors.getColor(rawData.indexOf(categoryIdx)));
118
+ const color = DG.Color.toHtml(DG.Color.categoricalPalette[categoryIdx % DG.Color.categoricalPalette.length]);
121
119
  const label = ui.label(labelMap[categories[categoryIdx]] ?? categories[categoryIdx], {style: {color}});
122
120
  labels.push(label);
123
121
  }
@@ -133,10 +131,8 @@ export function getDistributionPanel(hist: DG.Viewer<DG.IHistogramSettings>, sta
133
131
  * @param selection - Selection bitset.
134
132
  * @return - Dataframe with activity distribution.
135
133
  */
136
- export function getDistributionTable(activityCol: DG.Column<number>, selection: DG.BitSet): DG.DataFrame {
137
- if (!activityCol.dataFrame)
138
- DG.DataFrame.fromColumns([activityCol]); // to make sure that activityCol has a parent dataframe
139
- const filter = activityCol.dataFrame!.filter;
134
+ export function getDistributionTable(activityCol: DG.Column<number>, selection: DG.BitSet, dataFrame: DG.DataFrame): DG.DataFrame {
135
+ const filter = dataFrame.filter;
140
136
  const selectionAndFilter = selection.clone().and(filter);
141
137
  const rowCount = activityCol.length;
142
138
  const activityColData = activityCol.getRawData();
@@ -166,6 +162,44 @@ export function getDistributionTable(activityCol: DG.Column<number>, selection:
166
162
  return DG.DataFrame.fromColumns([DG.Column.fromFloat32Array(C.COLUMNS_NAMES.ACTIVITY, activityData), splitCol]);
167
163
  }
168
164
 
165
+ /**
166
+ * Creates distribution table for mutation cliffs.
167
+ * @param activityCol
168
+ * @param mpCliffs
169
+ */
170
+ export function getMutationCliffsDistributionTable(activityCol: DG.Column<number>, mpCliffs: Map<type.INDEX, type.INDEXES>, monomerName: string) {
171
+ // instead of stats.selection vs everything, here we want to show mutated vs non-mutated
172
+ if (!activityCol.dataFrame)
173
+ DG.DataFrame.fromColumns([activityCol]); // to make sure that activityCol has a parent dataframe
174
+ const tableFilter = activityCol.dataFrame!.filter;
175
+ const activityData = activityCol.getRawData();
176
+ const uniqueMPCliffsIndexes = Array.from(new Set(mpCliffs.keys())).map((k) => Number(k)).filter((i) => tableFilter.get(i));
177
+ const uniqueMutatedCliffsIndexesSet = new Set<number>();
178
+ for (const indexes of mpCliffs.values()) {
179
+ for (const index of indexes as number[])
180
+ uniqueMutatedCliffsIndexesSet.add(index);
181
+ }
182
+ const uniqueMutatedCliffsIndexes = Array.from(uniqueMutatedCliffsIndexesSet).filter((i) => tableFilter.get(i));
183
+ const totalRowCount = uniqueMPCliffsIndexes.length + uniqueMutatedCliffsIndexes.length;
184
+ const categories: string[] = new Array(totalRowCount);
185
+ const activityValues = new Float32Array(totalRowCount);
186
+ for (let i = 0; i < uniqueMPCliffsIndexes.length; ++i) {
187
+ const rowIndex = uniqueMPCliffsIndexes[i];
188
+ activityValues[i] = activityData[rowIndex];
189
+ categories[i] = monomerName;
190
+ }
191
+ for (let i = 0; i < uniqueMutatedCliffsIndexes.length; ++i) {
192
+ const rowIndex = uniqueMutatedCliffsIndexes[i];
193
+ activityValues[uniqueMPCliffsIndexes.length + i] = activityData[rowIndex];
194
+ categories[uniqueMPCliffsIndexes.length + i] = 'Mutated';
195
+ }
196
+ const splitCol = DG.Column.fromStrings(C.COLUMNS_NAMES.SPLIT_COL, categories);
197
+ const categoryOrder = [monomerName, 'Mutated'];
198
+ splitCol.setCategoryOrder(categoryOrder);
199
+ splitCol.meta.colors.setCategorical();
200
+ return DG.DataFrame.fromColumns([DG.Column.fromFloat32Array(C.COLUMNS_NAMES.ACTIVITY, activityValues), splitCol]);
201
+ }
202
+
169
203
  /**
170
204
  * Adds expand in full screen icon to the grid.
171
205
  * @param grid - Grid to add expand icon to.
@@ -6,7 +6,7 @@ import * as type from './types';
6
6
  import * as C from '../utils/constants';
7
7
 
8
8
  import {getActivityDistribution, getStatsTableMap} from '../widgets/distribution';
9
- import {getDistributionPanel, getDistributionTable} from './misc';
9
+ import {getDistributionPanel, getDistributionTable, getMutationCliffsDistributionTable} from './misc';
10
10
  import {getAggregatedColumnValues, MonomerPositionStats} from './statistics';
11
11
  import {StringDictionary} from '@datagrok-libraries/utils/src/type-declarations';
12
12
 
@@ -14,7 +14,7 @@ export type TooltipOptions = {
14
14
  fromViewer?: boolean, isMutationCliffs?: boolean, x: number, y: number, monomerPosition: type.SelectionItem,
15
15
  mpStats: MonomerPositionStats, aggrColValues?: StringDictionary,
16
16
  isMostPotentResidues?: boolean, cliffStats?: type.MutationCliffStats['stats'],
17
- postfixes?: StringDictionary, additionalStats?: StringDictionary
17
+ postfixes?: StringDictionary, additionalStats?: StringDictionary, cliffIndexes?: Map<type.INDEX, type.INDEXES>,
18
18
  };
19
19
 
20
20
  /**
@@ -52,7 +52,7 @@ export function showTooltipAt(df: DG.DataFrame, activityCol: DG.Column<number>,
52
52
  options.isMutationCliffs ??= false;
53
53
  options.isMostPotentResidues ??= false;
54
54
  options.additionalStats ??= {};
55
- if (!options.cliffStats || !options.isMutationCliffs) {
55
+ if (!options.cliffStats || !options.isMutationCliffs || !options.cliffIndexes) { // monomer position stats
56
56
  const stats = options
57
57
  .mpStats[options.monomerPosition.positionOrClusterType]![options.monomerPosition.monomerOrCluster];
58
58
  if (!stats?.count)
@@ -61,7 +61,7 @@ export function showTooltipAt(df: DG.DataFrame, activityCol: DG.Column<number>,
61
61
 
62
62
  const mask = DG.BitSet.fromBytes(stats.mask.buffer.buffer as ArrayBuffer, activityCol.length);
63
63
  mask.and(df.filter);
64
- const hist = getActivityDistribution(getDistributionTable(activityCol, mask), true);
64
+ const hist = getActivityDistribution(getDistributionTable(activityCol, mask, df), true);
65
65
  const tableMap = getStatsTableMap(stats);
66
66
  if (options.fromViewer) {
67
67
  tableMap['Mean difference'] = `${tableMap['Mean difference']}${options.isMostPotentResidues ? ' (size)' : ''}`;
@@ -79,7 +79,7 @@ export function showTooltipAt(df: DG.DataFrame, activityCol: DG.Column<number>,
79
79
  if (!options.fromViewer)
80
80
  setTimeout(() => hist.props.legendVisibility = 'Never', 100); // cause rerendering
81
81
  return distroStatsElem;
82
- } else {
82
+ } else { // mutation cliffs
83
83
  const stats = options.cliffStats?.get(options.monomerPosition.monomerOrCluster)
84
84
  ?.get(options.monomerPosition.positionOrClusterType)
85
85
  ;
@@ -87,12 +87,16 @@ export function showTooltipAt(df: DG.DataFrame, activityCol: DG.Column<number>,
87
87
  return null;
88
88
  const mask = DG.BitSet.fromBytes(stats.mask.buffer.buffer as ArrayBuffer, activityCol.length);
89
89
  mask.and(df.filter);
90
- const hist = getActivityDistribution(getDistributionTable(activityCol, mask), true);
91
- const tableMap = getStatsTableMap(stats, {countName: 'Unique count'});
90
+ const hist = getActivityDistribution(
91
+ getMutationCliffsDistributionTable(activityCol, options.cliffIndexes, options.monomerPosition.monomerOrCluster),
92
+ true);
93
+ const countName = 'MP in cliffs count';
94
+ const tableMap = getStatsTableMap(stats, {countName: countName});
92
95
  if (options.fromViewer) {
93
96
  tableMap['Mean difference'] = `${tableMap['Mean difference']}${' (Color)'}`;
94
- if (tableMap['Unique count'])
95
- tableMap['Unique count'] = `${tableMap['Unique count']}${' (Size)'}`;
97
+ if (tableMap[countName])
98
+ tableMap[countName] = `${tableMap[countName]}${' (Size)'}`;
99
+ options.additionalStats!['Unique count'] = mask.trueCount.toString();
96
100
  }
97
101
  const aggregatedColMap = options.aggrColValues ?? getAggregatedColumnValues(df, columns, {mask: mask});
98
102
  const resultMap = {...options.additionalStats, ...tableMap, ...aggregatedColMap};
@@ -8,8 +8,8 @@ import {getGPUAdapterDescription} from '@datagrok-libraries/math/src/webGPU/getG
8
8
  export type RawData = Int32Array | Uint32Array | Float32Array | Float64Array;
9
9
  type MONOMER = string;
10
10
  type POSITION = string;
11
- type INDEX = number;
12
- type INDEXES = number[] | UTypedArray;
11
+ export type INDEX = number;
12
+ export type INDEXES = number[] | UTypedArray;
13
13
  export type UTypedArray = Uint8Array | Uint16Array | Uint32Array;
14
14
  //Monomer: (Position: (index: indexList))
15
15
  export type MutationCliffs = Map<MONOMER, Map<POSITION, Map<INDEX, INDEXES>>>;
@@ -761,7 +761,9 @@ export class LogoSummaryTable extends DG.JsViewer implements ILogoSummaryTable {
761
761
  } else if (gridCell.tableColumn?.name === C.LST_COLUMN_NAMES.DISTRIBUTION) {
762
762
  let viewer = distCache.get(currentRowIdx);
763
763
  if (viewer === undefined) {
764
- const distributionDf = getDistributionTable(activityCol, clusterBitSet);
764
+ if (!activityCol.dataFrame)
765
+ DG.DataFrame.fromColumns([activityCol]);
766
+ const distributionDf = getDistributionTable(activityCol, clusterBitSet, activityCol.dataFrame);
765
767
  viewer = distributionDf.plot.histogram({
766
768
  filteringEnabled: false,
767
769
  valueColumnName: activityCol.name,
@@ -1070,7 +1072,9 @@ export class LogoSummaryTable extends DG.JsViewer implements ILogoSummaryTable {
1070
1072
 
1071
1073
 
1072
1074
  const mask = DG.BitSet.fromBytes(bitArray.buffer.buffer as ArrayBuffer, rowCount);
1073
- const distributionTable = getDistributionTable(activityCol, mask);
1075
+ if (!activityCol.dataFrame)
1076
+ DG.DataFrame.fromColumns([activityCol]);
1077
+ const distributionTable = getDistributionTable(activityCol, mask, activityCol.dataFrame);
1074
1078
  const hist = getActivityDistribution(distributionTable, true);
1075
1079
  const tableMap = getStatsTableMap(stats);
1076
1080
  const aggregatedColMap = getAggregatedColumnValues(this.dataFrame,
@@ -20,6 +20,7 @@ export class MutationCliffsViewer extends DG.JsViewer {
20
20
  public seriesColumnName: string;
21
21
  public activityColumnName: string;
22
22
  public position = 1;
23
+ public currentRowMutationsOnly: boolean = false;
23
24
  public yAxisType: 'Linear' | 'Logarithmic' = 'Linear';
24
25
  constructor() {
25
26
  super();
@@ -28,6 +29,7 @@ export class MutationCliffsViewer extends DG.JsViewer {
28
29
  this.activityColumnName = this.column('activity', {columnTypeFilter: 'numerical', nullable: false});
29
30
  this.position = this.int('position', 1, {nullable: false, showSlider: false, min: 1, max: 100, showPlusMinus: true, description: 'Position in the sequence to analyze (1 Based).', category: 'Data'});
30
31
  this.yAxisType = this.string('yAxisType', 'Linear', {choices: ['Linear', 'Logarithmic'], description: 'Y-Axis scale type.', nullable: false, category: 'Data'}) as 'Linear' | 'Logarithmic';
32
+ this.currentRowMutationsOnly = this.bool('currentRowMutationsOnly', false, {nullable: false, defaultValue: false, description: 'When enabled, the viewer will show mutations related to the peptide in current row in the dataframe and selected position.', category: 'Data'});
31
33
  }
32
34
 
33
35
  onTableAttached(): void {
@@ -105,8 +107,11 @@ export class MutationCliffsViewer extends DG.JsViewer {
105
107
  mutationCliffs.cliffs.forEach((positionMap) => {
106
108
  positionMap.forEach((mcData) => { // should be only one position
107
109
  Array.from(mcData.entries()).forEach(([from, toIndexes]) => {
108
- uniqueIndexes.add(Number(from));
109
- toIndexes.forEach((toIdx) => uniqueIndexes.add(Number(toIdx)));
110
+ const f = Number(from);
111
+ if (!this.currentRowMutationsOnly || this.dataFrame.currentRowIdx === f) {
112
+ uniqueIndexes.add(f);
113
+ toIndexes.forEach((toIdx) => uniqueIndexes.add(Number(toIdx)));
114
+ }
110
115
  });
111
116
  });
112
117
  });
@@ -160,14 +165,31 @@ export class MutationCliffsViewer extends DG.JsViewer {
160
165
  }),
161
166
  );
162
167
 
168
+ let currentRowFromLineChartFired = false;
169
+ // when clicking on point in line chart, set current row in main df
163
170
  this._dfSubs.push(
164
171
  df.onCurrentRowChanged.subscribe((_) => {
165
172
  if (df.currentRowIdx >= 0) {
173
+ currentRowFromLineChartFired = true;
166
174
  const originalIdx = indexesArray[df.currentRowIdx];
167
175
  this.dataFrame.currentRowIdx = originalIdx;
168
176
  }
169
177
  }),
170
178
  );
179
+ // if the viewer is set to follow current row, listen to it
180
+ if (this.currentRowMutationsOnly) {
181
+ this._dfSubs.push(
182
+ this.dataFrame.onCurrentRowChanged.subscribe((_) => {
183
+ if (currentRowFromLineChartFired) {
184
+ currentRowFromLineChartFired = false;
185
+ return;
186
+ }
187
+ // recalculate viewer df
188
+ this.clearCache(false);
189
+ this.debouncedRender();
190
+ }));
191
+ }
192
+
171
193
 
172
194
  let firedFromViewer = false;
173
195
  let firedFromTable = false;
@@ -272,7 +294,8 @@ export class MutationCliffsViewer extends DG.JsViewer {
272
294
  private async render() {
273
295
  $(this.root).empty();
274
296
  if (!this.dataFrame || !this.activityColumnName || !this.sequenceColumnName || !this.position) {
275
- this.root.appendChild(ui.divText('Please set Activity column, Sequence column and Position properties.'));
297
+ ui.setUpdateIndicator(this.root, false);
298
+ this.root.appendChild(noDataDiv('Please set Activity column, Sequence column and Position properties.'));
276
299
  return;
277
300
  }
278
301
 
@@ -290,32 +313,40 @@ export class MutationCliffsViewer extends DG.JsViewer {
290
313
  this.root.style.flexDirection = 'column';
291
314
 
292
315
  const df = await this.innerDf;
293
-
294
- this._lineChart = df.plot.line({
295
- xColumnName: `Position ${this.position}`,
296
- yColumnNames: [this.activityColumnName],
297
- splitColumnNames: this.seriesColumnName ? [this.seriesColumnName] : [],
298
- legendVisibility: this.seriesColumnName ? 'Always' : 'Never',
299
- legendPosition: 'Right',
300
- showXSelector: false,
301
- showYSelector: true,
302
- showSplitSelector: false,
303
- xAxisLabelOrientation: 'Auto',
304
- axisFont: 'normal normal 14px "Roboto"',
305
- controlsFont: 'normal normal 14px "Roboto"',
306
- } as Partial<DG.ILineChartSettings>) as DG.LineChartViewer;
307
-
308
-
309
- this._lineChart.sub(this._lineChart.onPropertyValueChanged.subscribe((_e) => {
310
- if (this._lineChart?.props?.yColumnNames && this._lineChart?.props?.yColumnNames?.[0] !== this.activityColumnName) {
311
- const value = this._lineChart?.props?.yColumnNames?.[0];
312
- setTimeout(() => this.getProperty('activityColumnName')!.set(this, value), 1);
313
- }
314
- }));
315
-
316
316
  ui.setUpdateIndicator(this.root, false);
317
- this.root.appendChild(this._lineChart.root);
317
+ if (df.rowCount === 0) {
318
+ if (this.currentRowMutationsOnly) {
319
+ if (this.dataFrame.currentRowIdx >= 0)
320
+ this.root.appendChild(noDataDiv('No mutations cliffs found for the current peptide at the selected position.'));
321
+ else
322
+ this.root.appendChild(noDataDiv('Please select a row in the main table to see mutation cliffs for the corresponding peptide at the selected position.'));
323
+ } else
324
+ this.root.appendChild(noDataDiv('No mutation cliffs found for the selected position.'));
325
+ } else {
326
+ this._lineChart = df.plot.line({
327
+ xColumnName: `Position ${this.position}`,
328
+ yColumnNames: [this.activityColumnName],
329
+ splitColumnNames: this.seriesColumnName ? [this.seriesColumnName] : [],
330
+ legendVisibility: this.seriesColumnName ? 'Always' : 'Never',
331
+ legendPosition: 'Right',
332
+ showXSelector: false,
333
+ showYSelector: true,
334
+ showSplitSelector: false,
335
+ xAxisLabelOrientation: 'Auto',
336
+ axisFont: 'normal normal 14px "Roboto"',
337
+ controlsFont: 'normal normal 14px "Roboto"',
338
+ } as Partial<DG.ILineChartSettings>) as DG.LineChartViewer;
339
+
340
+
341
+ this._lineChart.sub(this._lineChart.onPropertyValueChanged.subscribe((_e) => {
342
+ if (this._lineChart?.props?.yColumnNames && this._lineChart?.props?.yColumnNames?.[0] !== this.activityColumnName) {
343
+ const value = this._lineChart?.props?.yColumnNames?.[0];
344
+ setTimeout(() => this.getProperty('activityColumnName')!.set(this, value), 1);
345
+ }
346
+ }));
318
347
 
348
+ this.root.appendChild(this._lineChart.root);
349
+ }
319
350
  const maxPosition = this.positionColumns.length + 1;
320
351
  const positions = new Array<number>(maxPosition - 1).fill(0).map((_, i) => i + 1).map((v) => v.toString());
321
352
  const positionInput = ui.input.choice('Position', {value: this.position.toString(), items: positions, nullable: false,
@@ -330,16 +361,18 @@ export class MutationCliffsViewer extends DG.JsViewer {
330
361
  positionInput.input.style.width = '40px';
331
362
  this.root.appendChild(ui.divH([positionInput.root], {style: {justifyContent: 'center', marginTop: '4px', width: '100%', font: 'normal normal 14px "Roboto"'}}));
332
363
 
333
- const seriesColSelector = ui.input.column('Series', {table: this.dataFrame,
334
- filter: (col: DG.Column) => {
335
- return col.isCategorical && col.name !== this.sequenceColumnName;
336
- }, tooltipText: 'Select column for series splitting.',
337
- onValueChanged: (col: DG.Column | null) => {
338
- const colName = col ? col.name : undefined;
364
+ if (df.rowCount > 0) {
365
+ const seriesColSelector = ui.input.column('Series', {table: this.dataFrame,
366
+ filter: (col: DG.Column) => {
367
+ return col.isCategorical && col.name !== this.sequenceColumnName;
368
+ }, tooltipText: 'Select column for series splitting.',
369
+ onValueChanged: (col: DG.Column | null) => {
370
+ const colName = col ? col.name : undefined;
339
371
  this.getProperty('seriesColumnName')!.set(this, colName);
340
- }, value: this.seriesColumnName ? this.dataFrame.col(this.seriesColumnName)! : undefined,
341
- });
342
- this.root.prepend(ui.divH([seriesColSelector.root], {style: {justifyContent: 'flex-end', paddingBottom: '4px', padding: '8px', width: '100%', font: 'normal normal 14px "Roboto"'}}));
372
+ }, value: this.seriesColumnName ? this.dataFrame.col(this.seriesColumnName)! : undefined,
373
+ });
374
+ this.root.prepend(ui.divH([seriesColSelector.root], {style: {justifyContent: 'flex-end', paddingBottom: '4px', padding: '8px', width: '100%', font: 'normal normal 14px "Roboto"'}}));
375
+ }
343
376
  }
344
377
 
345
378
  private _debounceTimer: any = null;
@@ -350,8 +383,10 @@ export class MutationCliffsViewer extends DG.JsViewer {
350
383
  this._debounceTimer = setTimeout(() => this.render(), 300);
351
384
  }
352
385
 
353
- private clearCache() {
354
- this._mutationCliffsData = null;
386
+ private clearCache(clearMutationCliffsData: boolean = true) {
387
+ // while following current row, makes no sense to clear mutation cliffs data.
388
+ if (clearMutationCliffsData)
389
+ this._mutationCliffsData = null;
355
390
  this._innerDf = null;
356
391
  this._dfSubs.forEach((s) => s.unsubscribe());
357
392
  this._dfSubs = [];
@@ -368,11 +403,27 @@ export class MutationCliffsViewer extends DG.JsViewer {
368
403
  onPropertyChanged(property: DG.Property | null): void {
369
404
  super.onPropertyChanged(property);
370
405
  if (property?.name === 'activityColumnName' || property?.name === 'sequenceColumnName' || property?.name === 'position' || property?.name === 'seriesColumnName') {
371
- this.clearCache();
406
+ const retainMutationCliffsData = property?.name === 'seriesColumnName' || property?.name === 'activityColumnName'; // these do not affect mutation cliffs calculation
407
+ this.clearCache(!retainMutationCliffsData);
408
+ if (property?.name === 'sequenceColumnName')
409
+ this._positionColumns = null;
410
+
372
411
  this.debouncedRender();
373
- } if (property?.name === 'yAxisType') {
412
+ } else if (property?.name === 'yAxisType') {
374
413
  if (this._lineChart)
375
414
  this._lineChart.props.yAxisType = this.yAxisType.toLowerCase() as DG.AxisType;
415
+ } else if (property?.name === 'currentRowMutationsOnly') {
416
+ // when this changes, we only clear the dataframe cache to reflect current row changes
417
+ this.clearCache(false);
418
+ this.debouncedRender();
376
419
  }
377
420
  }
378
421
  }
422
+
423
+ function noDataDiv(message: string): HTMLDivElement {
424
+ const noDataDiv = ui.divText(message);
425
+ noDataDiv.style.fontSize = '14px';
426
+ noDataDiv.style.marginTop = '10px';
427
+ noDataDiv.style.textAlign = 'center';
428
+ return noDataDiv;
429
+ }
@@ -131,7 +131,7 @@ export abstract class SARViewer extends DG.JsViewer implements ISARViewer {
131
131
  }) as 'All' | 'Filtered';
132
132
 
133
133
  this.activityColumnName = this.column(SAR_PROPERTIES.ACTIVITY,
134
- {category: PROPERTY_CATEGORIES.GENERAL, nullable: false});
134
+ {category: PROPERTY_CATEGORIES.GENERAL, nullable: false, columnTypeFilter: 'numerical'});
135
135
  this.activityScaling = this.string(SAR_PROPERTIES.ACTIVITY_SCALING, C.SCALING_METHODS.NONE,
136
136
  {category: PROPERTY_CATEGORIES.GENERAL, choices: Object.values(C.SCALING_METHODS), nullable: false},
137
137
  ) as C.SCALING_METHODS;
@@ -555,10 +555,11 @@ export abstract class SARViewer extends DG.JsViewer implements ISARViewer {
555
555
  if (isApplicableDataframe(this.dataFrame)) {
556
556
  this.getProperty(`${SAR_PROPERTIES.SEQUENCE}${COLUMN_NAME}`)
557
557
  ?.set(this, this.dataFrame.columns.bySemType(DG.SEMTYPE.MACROMOLECULE)!.name);
558
+ const potentialActivityColumn = wu(this.dataFrame.columns.numerical).find((col) => col.name.toLowerCase().includes('activity'))?.name;
558
559
  this.getProperty(`${SAR_PROPERTIES.ACTIVITY}${COLUMN_NAME}`)
559
- ?.set(this, wu(this.dataFrame.columns.numerical).next().value.name);
560
+ ?.set(this, potentialActivityColumn ?? wu(this.dataFrame.columns.numerical).next().value.name);
560
561
  this.getProperty(`${SAR_PROPERTIES.VALUE_INVARIANT_MAP}${COLUMN_NAME}`)
561
- ?.set(this, wu(this.dataFrame.columns.numerical).next().value.name);
562
+ ?.set(this, potentialActivityColumn ?? wu(this.dataFrame.columns.numerical).next().value.name);
562
563
  if (this.mutationCliffs === null && this.sequenceColumnName && this.activityColumnName)
563
564
  this.calculateMutationCliffs().then((mc) => {this.mutationCliffs = mc.cliffs; this.cliffStats = mc.cliffStats;});
564
565
  this.subs.push(grok.events.onContextMenu.subscribe((a: DG.EventData) => {
@@ -934,7 +935,7 @@ export class MonomerPosition extends SARViewer {
934
935
  if (this.valueColumnName && this.valueAggregation && this.valueAggregation !== DG.AGG.VALUE_COUNT && this.valueAggregation !== DG.AGG.TOTAL_COUNT)
935
936
  columnEntries.unshift([this.valueColumnName, this.valueAggregation as DG.AGG]);
936
937
  } else {
937
- // in invariant map, show pairs count along with unique sequences count
938
+ // in mutation cliffs, show pairs count along with unique sequences count
938
939
  const pairs = this.mutationCliffs?.get(monomerPosition.monomerOrCluster)?.get(monomerPosition.positionOrClusterType);
939
940
  if (pairs) {
940
941
  let pairsCount = 0;
@@ -947,6 +948,7 @@ export class MonomerPosition extends SARViewer {
947
948
  fromViewer: true,
948
949
  isMutationCliffs: this.mode === SELECTION_MODE.MUTATION_CLIFFS, monomerPosition, x, y,
949
950
  mpStats: this.monomerPositionStats, cliffStats: this.cliffStats?.stats ?? undefined, postfixes, additionalStats,
951
+ cliffIndexes: this.mutationCliffs?.get(monomerPosition.monomerOrCluster)?.get(monomerPosition.positionOrClusterType),
950
952
  });
951
953
  });
952
954
  grid.root.addEventListener('mouseleave', (_ev) => this.model.unhighlight());
@@ -144,7 +144,7 @@ export function getStatsTableMap(stats: StatsItem,
144
144
  */
145
145
  function getSingleDistribution(table: DG.DataFrame, stats: StatsItem, options: DistributionItemOptions,
146
146
  labelMap: DistributionLabelMap = {}): HTMLDivElement {
147
- const hist = getActivityDistribution(getDistributionTable(options.activityCol, table.selection));
147
+ const hist = getActivityDistribution(getDistributionTable(options.activityCol, table.selection, table));
148
148
  const aggregatedColMap = getAggregatedColumnValues(table, Object.entries(options.columns),
149
149
  {filterDf: true, mask: DG.BitSet.fromBytes(stats.mask.buffer.buffer as ArrayBuffer, stats.mask.length)});
150
150
  const tableMap = getStatsTableMap(stats);
@@ -3,12 +3,14 @@ import * as ui from 'datagrok-api/ui';
3
3
  import * as DG from 'datagrok-api/dg';
4
4
  import * as C from '../utils/constants';
5
5
  import * as type from '../utils/types';
6
- import {addExpandIconGen, getSeparator, setGridProps} from '../utils/misc';
6
+ import {addExpandIconGen, getDistributionPanel, getMutationCliffsDistributionTable, getSeparator, setGridProps} from '../utils/misc';
7
7
  import {renderCellSelection} from '../utils/cell-renderer';
8
8
  import {SeqTemps} from '@datagrok-libraries/bio/src/utils/macromolecule/seq-handler';
9
+ import {getActivityDistribution, getStatsTableMap} from './distribution';
10
+ import {StringDictionary} from '@datagrok-libraries/utils/src/type-declarations';
9
11
 
10
- export type MutationCliffsOptions = {
11
- mutationCliffs: type.MutationCliffs, mutationCliffsSelection: type.Selection, sequenceColumnName: string,
12
+ export type MutationCliffsWidgetOptions = {
13
+ mutationCliffs: type.MutationCliffs, mutationCliffStats: type.MutationCliffStats | null, mutationCliffsSelection: type.Selection, sequenceColumnName: string,
12
14
  positionColumns: DG.Column<string>[], gridColumns: DG.GridColumnList, activityCol: DG.Column<number>,
13
15
  };
14
16
 
@@ -21,7 +23,7 @@ export type MutationCliffsOptions = {
21
23
  * @return - mutation cliffs widget.
22
24
  */
23
25
  export function mutationCliffsWidget(
24
- table: DG.DataFrame, options: MutationCliffsOptions, allowExpand = true,
26
+ table: DG.DataFrame, options: MutationCliffsWidgetOptions, allowExpand = true,
25
27
  ): DG.Widget {
26
28
  //addExpandIcon(pairsGrid);
27
29
  //addExpandIcon(uniqueSequencesGrid);
@@ -32,6 +34,37 @@ export function mutationCliffsWidget(
32
34
  const comboGrids = [pairsGrid, uniqueSequencesGrid];
33
35
  const widgetRoot = ui.divV([aminoToInput.root, ...comboGrids.map((grid) => grid.root)],
34
36
  {style: {width: '100%'}});
37
+ // add mutation cliffs distribution histogram
38
+ const isSingleMutationPosition = Object.values(options.mutationCliffsSelection).filter((monomers) => monomers.length > 0).length === 1;
39
+ if (isSingleMutationPosition) {
40
+ const position = Object.keys(options.mutationCliffsSelection).find((pos) => options.mutationCliffsSelection[pos].length > 0)!;
41
+ const monomers = options.mutationCliffsSelection[position];
42
+ if (monomers.length === 1) {
43
+ const monomerName = monomers[0];
44
+ const cliffs = options.mutationCliffs?.get(monomerName)?.get(position);
45
+ if (cliffs && cliffs.size > 0) {
46
+ const distributionTable = getMutationCliffsDistributionTable(options.activityCol, cliffs, monomerName);
47
+ const hist = getActivityDistribution(distributionTable, false);
48
+ // quick stats
49
+ const stats = options.mutationCliffStats?.stats?.get(monomerName)?.get(position);
50
+ const tableMap = {} as StringDictionary;
51
+ let pairsCount = 0;
52
+ for (const indexArray of cliffs.values())
53
+ pairsCount += indexArray.length;
54
+ tableMap['Pairs count'] = pairsCount.toString();
55
+ if (stats) {
56
+ tableMap['Unique Count'] = stats.mask.trueCount().toString();
57
+ Object.assign(tableMap, getStatsTableMap(stats, {countName: 'MP in cliffs count'}));
58
+ }
59
+ const distributionPanel = getDistributionPanel(hist, tableMap); // no need to show stats here
60
+ hist.root.style.maxHeight = '100px';
61
+ distributionPanel.style.marginTop = '10px';
62
+ widgetRoot.appendChild(distributionPanel);
63
+ }
64
+ }
65
+ }
66
+
67
+
35
68
  if (allowExpand) {
36
69
  addExpandIconGen('Mutation Cliffs pairs', aminoToInput.root, widgetRoot,
37
70
  () => {
@@ -74,7 +107,7 @@ export function mutationCliffsWidget(
74
107
  }
75
108
 
76
109
 
77
- function cliffsPairsWidgetParts(table: DG.DataFrame, options: MutationCliffsOptions):
110
+ function cliffsPairsWidgetParts(table: DG.DataFrame, options: MutationCliffsWidgetOptions):
78
111
  {pairsGrid: DG.Grid, uniqueSequencesGrid: DG.Grid, aminoToInput: DG.InputBase<string>} | null {
79
112
  const filteredIndexes = table.filter.getSelectedIndexes();
80
113
  const positions = Object.keys(options.mutationCliffsSelection);