@datagrok/peptides 1.15.3 → 1.16.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/CHANGELOG.md +14 -0
- package/dist/package-test.js +2 -2
- package/dist/package.js +2 -2
- package/package.json +1 -1
- package/src/model.ts +21 -196
- package/src/tests/model.ts +2 -1
- package/src/tests/table-view.ts +2 -1
- package/src/tests/viewers.ts +5 -4
- package/src/utils/algorithms.ts +13 -111
- package/src/utils/cell-renderer.ts +122 -6
- package/src/utils/constants.ts +1 -0
- package/src/utils/statistics.ts +38 -1
- package/src/utils/tooltips.ts +78 -0
- package/src/utils/types.ts +2 -0
- package/src/viewers/logo-summary.ts +18 -4
- package/src/viewers/sar-viewer.ts +53 -8
- package/src/widgets/distribution.ts +9 -6
- package/src/widgets/selection.ts +14 -0
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import * as ui from 'datagrok-api/ui';
|
|
1
2
|
import * as DG from 'datagrok-api/dg';
|
|
2
3
|
|
|
3
4
|
import * as C from './constants';
|
|
4
|
-
import * as
|
|
5
|
-
import {
|
|
5
|
+
import * as type from './types';
|
|
6
|
+
import {CLUSTER_TYPE, PeptidesModel} from '../model';
|
|
6
7
|
import {SeqPalette} from '@datagrok-libraries/bio/src/seq-palettes';
|
|
7
8
|
import {monomerToShort} from '@datagrok-libraries/bio/src/utils/macromolecule';
|
|
9
|
+
import {calculateMonomerPositionStatistics} from './algorithms';
|
|
10
|
+
import * as rxjs from 'rxjs';
|
|
11
|
+
import {TooltipOptions, showTooltipAt} from './tooltips';
|
|
12
|
+
import {MonomerPositionStats, PositionStats} from './statistics';
|
|
8
13
|
|
|
9
14
|
export function renderCellSelection(canvasContext: CanvasRenderingContext2D, bound: DG.Rect): void {
|
|
10
15
|
canvasContext.strokeStyle = '#000';
|
|
@@ -21,7 +26,7 @@ export function setMonomerRenderer(col: DG.Column, alphabet: string): void {
|
|
|
21
26
|
|
|
22
27
|
export function renderMutationCliffCell(canvasContext: CanvasRenderingContext2D, currentMonomer: string,
|
|
23
28
|
currentPosition: string, monomerPositionStats: MonomerPositionStats, bound: DG.Rect,
|
|
24
|
-
mutationCliffsSelection:
|
|
29
|
+
mutationCliffsSelection: type.Selection, substitutionsInfo: type.MutationCliffs | null = null,
|
|
25
30
|
renderNums: boolean = true): void {
|
|
26
31
|
const positionStats = monomerPositionStats[currentPosition];
|
|
27
32
|
const pVal = positionStats![currentMonomer]!.pValue;
|
|
@@ -79,7 +84,7 @@ export function renderMutationCliffCell(canvasContext: CanvasRenderingContext2D,
|
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
export function renderInvaraintMapCell(canvasContext: CanvasRenderingContext2D, currentMonomer: string,
|
|
82
|
-
currentPosition: string, invariantMapSelection:
|
|
87
|
+
currentPosition: string, invariantMapSelection: type.Selection, cellValue: number, bound: DG.Rect,
|
|
83
88
|
color: number): void {
|
|
84
89
|
//FIXME: This is a hack, because `color` value sometimes comes incomplete. E.g. we found that here `color` value is
|
|
85
90
|
// 255 and its contrast color would be black, which is not visible on blue (color code) background. The full number
|
|
@@ -99,7 +104,7 @@ export function renderInvaraintMapCell(canvasContext: CanvasRenderingContext2D,
|
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
export function renderLogoSummaryCell(canvasContext: CanvasRenderingContext2D, cellValue: string,
|
|
102
|
-
clusterSelection:
|
|
107
|
+
clusterSelection: type.Selection, bound: DG.Rect): void {
|
|
103
108
|
canvasContext.font = '13px Roboto, Roboto Local, sans-serif';
|
|
104
109
|
canvasContext.textAlign = 'center';
|
|
105
110
|
canvasContext.textBaseline = 'middle';
|
|
@@ -113,7 +118,7 @@ export function renderLogoSummaryCell(canvasContext: CanvasRenderingContext2D, c
|
|
|
113
118
|
|
|
114
119
|
export function drawLogoInBounds(ctx: CanvasRenderingContext2D, bounds: DG.Rect, stats: PositionStats, position: string,
|
|
115
120
|
sortedOrder: string[], rowCount: number, cp: SeqPalette, monomerSelectionStats: { [monomer: string]: number } = {},
|
|
116
|
-
drawOptions:
|
|
121
|
+
drawOptions: type.DrawOptions = {}): { [monomer: string]: DG.Rect } {
|
|
117
122
|
const pr = window.devicePixelRatio;
|
|
118
123
|
drawOptions.symbolStyle ??= '16px Roboto, Roboto Local, sans-serif';
|
|
119
124
|
drawOptions.upperLetterHeight ??= 12.2;
|
|
@@ -172,3 +177,114 @@ export function drawLogoInBounds(ctx: CanvasRenderingContext2D, bounds: DG.Rect,
|
|
|
172
177
|
|
|
173
178
|
return monomerBounds;
|
|
174
179
|
}
|
|
180
|
+
|
|
181
|
+
export type CellRendererOptions = {isSelectionTable?: boolean, headerSelectedMonomers?: type.SelectionStats,
|
|
182
|
+
webLogoBounds?: WebLogoBounds, cachedWebLogoTooltip?: type.CachedWebLogoTooltip};
|
|
183
|
+
export type WebLogoBounds = {[positon: string]: {[monomer: string]: DG.Rect}};
|
|
184
|
+
|
|
185
|
+
export function setWebLogoRenderer(grid: DG.Grid, model: PeptidesModel, options: CellRendererOptions = {},
|
|
186
|
+
tooltipOptions: TooltipOptions = {x: 0, y: 0, mpStats: {} as MonomerPositionStats, monomerPosition: {} as type.SelectionItem}): void {
|
|
187
|
+
options.isSelectionTable ??= false;
|
|
188
|
+
if (Object.keys(tooltipOptions.mpStats).length == 0)
|
|
189
|
+
tooltipOptions.mpStats = model.monomerPositionStats;
|
|
190
|
+
if (options.isSelectionTable && (!options.webLogoBounds || !options.cachedWebLogoTooltip)) {
|
|
191
|
+
throw new Error('Peptides: Cannot set WebLogo renderer for selection table without `headerSelectedMonomers`, ' +
|
|
192
|
+
'`webLogoBounds` and `cachedWebLogoTooltip` options.');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
options.webLogoBounds ??= model.webLogoBounds;
|
|
196
|
+
options.cachedWebLogoTooltip ??= model.cachedWebLogoTooltip;
|
|
197
|
+
const df = grid.dataFrame;
|
|
198
|
+
grid.setOptions({'colHeaderHeight': 130});
|
|
199
|
+
const headerRenderer = (gcArgs: DG.GridCellRenderArgs): void => {
|
|
200
|
+
const ctx = gcArgs.g;
|
|
201
|
+
const bounds = gcArgs.bounds;
|
|
202
|
+
const col = gcArgs.cell.tableColumn;
|
|
203
|
+
|
|
204
|
+
ctx.save();
|
|
205
|
+
try {
|
|
206
|
+
ctx.beginPath();
|
|
207
|
+
ctx.rect(bounds.x, bounds.y, bounds.width, bounds.height);
|
|
208
|
+
ctx.clip();
|
|
209
|
+
|
|
210
|
+
//TODO: optimize
|
|
211
|
+
if (gcArgs.cell.isColHeader && col?.semType === C.SEM_TYPES.MONOMER) {
|
|
212
|
+
const isDfFiltered = df.filter.anyFalse;
|
|
213
|
+
const stats = (isDfFiltered || options.isSelectionTable ?
|
|
214
|
+
calculateMonomerPositionStatistics(df, model.positionColumns.toArray(), {isFiltered: true, columns: [col.name]}) :
|
|
215
|
+
model.monomerPositionStats)[col.name];
|
|
216
|
+
if (!stats)
|
|
217
|
+
return;
|
|
218
|
+
//TODO: precalc on stats creation
|
|
219
|
+
const sortedStatsOrder = Object.keys(stats).sort((a, b) => {
|
|
220
|
+
if (a === '' || a === '-')
|
|
221
|
+
return +1;
|
|
222
|
+
else if (b === '' || b === '-')
|
|
223
|
+
return -1;
|
|
224
|
+
return 0;
|
|
225
|
+
}).filter((v) => v !== 'general');
|
|
226
|
+
|
|
227
|
+
options.webLogoBounds![col.name] = drawLogoInBounds(ctx, bounds, stats, col.name,
|
|
228
|
+
sortedStatsOrder, df.filter.trueCount, model.cp, options.headerSelectedMonomers ? options.headerSelectedMonomers[col.name] : {});
|
|
229
|
+
gcArgs.preventDefault();
|
|
230
|
+
}
|
|
231
|
+
} catch (e) {
|
|
232
|
+
console.warn(`PeptidesHeaderLogoError: couldn't render WebLogo for column \`${col!.name}\`. ` +
|
|
233
|
+
`See original error below.`);
|
|
234
|
+
console.warn(e);
|
|
235
|
+
} finally {
|
|
236
|
+
ctx.restore();
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
grid.onCellRender.subscribe((gcArgs) => headerRenderer(gcArgs));
|
|
240
|
+
|
|
241
|
+
const eventAction = (ev: MouseEvent): void => {
|
|
242
|
+
const cell = grid.hitTest(ev.offsetX, ev.offsetY);
|
|
243
|
+
if (cell?.isColHeader && cell.tableColumn?.semType === C.SEM_TYPES.MONOMER) {
|
|
244
|
+
const monomerPosition = findWebLogoMonomerPosition(cell, ev, (options.webLogoBounds ?? model.webLogoBounds));
|
|
245
|
+
if (monomerPosition === null) {
|
|
246
|
+
if (!options.isSelectionTable)
|
|
247
|
+
model.unhighlight();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
tooltipOptions.monomerPosition = monomerPosition;
|
|
251
|
+
requestBarchartAction(ev, monomerPosition, model, df, options, tooltipOptions);
|
|
252
|
+
if (!options.isSelectionTable)
|
|
253
|
+
model.highlightMonomerPosition(monomerPosition);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// The following events makes the barchart interactive
|
|
258
|
+
rxjs.fromEvent<MouseEvent>(grid.overlay, 'mousemove').subscribe((mouseMove: MouseEvent) => eventAction(mouseMove));
|
|
259
|
+
rxjs.fromEvent<MouseEvent>(grid.overlay, 'click').subscribe((mouseMove: MouseEvent) => eventAction(mouseMove));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function requestBarchartAction(ev: MouseEvent, monomerPosition: type.SelectionItem, model: PeptidesModel, df: DG.DataFrame,
|
|
263
|
+
options: CellRendererOptions, tooltipOptions: TooltipOptions): void {
|
|
264
|
+
if (ev.type === 'click' && !options.isSelectionTable)
|
|
265
|
+
model.modifyInvariantMapSelection(monomerPosition, {shiftPressed: ev.shiftKey, ctrlPressed: ev.ctrlKey});
|
|
266
|
+
else {
|
|
267
|
+
const bar = `${monomerPosition.positionOrClusterType} = ${monomerPosition.monomerOrCluster}`;
|
|
268
|
+
if (options.cachedWebLogoTooltip!.bar === bar)
|
|
269
|
+
ui.tooltip.show(options.cachedWebLogoTooltip!.tooltip!, ev.clientX, ev.clientY);
|
|
270
|
+
else {
|
|
271
|
+
options.cachedWebLogoTooltip!.bar = bar;
|
|
272
|
+
tooltipOptions.x = ev.clientX;
|
|
273
|
+
tooltipOptions.y = ev.clientY;
|
|
274
|
+
tooltipOptions.monomerPosition = monomerPosition;
|
|
275
|
+
options.cachedWebLogoTooltip!.tooltip = showTooltipAt(df, model.settings.columns!, tooltipOptions);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function findWebLogoMonomerPosition(cell: DG.GridCell, ev: MouseEvent, webLogoBounds: WebLogoBounds): type.SelectionItem | null {
|
|
281
|
+
const barCoords = webLogoBounds[cell.tableColumn!.name];
|
|
282
|
+
for (const [monomer, coords] of Object.entries(barCoords)) {
|
|
283
|
+
const isIntersectingX = ev.offsetX >= coords.x && ev.offsetX <= coords.x + coords.width;
|
|
284
|
+
const isIntersectingY = ev.offsetY >= coords.y && ev.offsetY <= coords.y + coords.height;
|
|
285
|
+
if (isIntersectingX && isIntersectingY)
|
|
286
|
+
return {monomerOrCluster: monomer, positionOrClusterType: cell.tableColumn!.name};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return null;
|
|
290
|
+
}
|
package/src/utils/constants.ts
CHANGED
package/src/utils/statistics.ts
CHANGED
|
@@ -2,6 +2,8 @@ import * as DG from 'datagrok-api/dg';
|
|
|
2
2
|
import {tTest} from '@datagrok-libraries/statistics/src/tests';
|
|
3
3
|
import {RawData} from './types';
|
|
4
4
|
import BitArray from '@datagrok-libraries/utils/src/bit-array';
|
|
5
|
+
import {StringDictionary} from '@datagrok-libraries/utils/src/type-declarations';
|
|
6
|
+
import {ClusterType} from '../model';
|
|
5
7
|
|
|
6
8
|
export type Stats = {
|
|
7
9
|
count: number,
|
|
@@ -9,8 +11,25 @@ export type Stats = {
|
|
|
9
11
|
meanDifference: number,
|
|
10
12
|
ratio: number,
|
|
11
13
|
mask: BitArray,
|
|
14
|
+
mean: number,
|
|
12
15
|
};
|
|
13
16
|
|
|
17
|
+
export type PositionStats = {[monomer: string]: Stats | undefined} & {general: SummaryStats};
|
|
18
|
+
export type MonomerPositionStats = {[position: string]: PositionStats | undefined} & {general: SummaryStats};
|
|
19
|
+
export type ClusterStats = {[cluster: string]: Stats};
|
|
20
|
+
export type ClusterTypeStats = {[clusterType in ClusterType]: ClusterStats};
|
|
21
|
+
|
|
22
|
+
export type SummaryStats = {
|
|
23
|
+
minCount: number, maxCount: number,
|
|
24
|
+
minMeanDifference: number, maxMeanDifference: number,
|
|
25
|
+
minPValue: number, maxPValue: number,
|
|
26
|
+
minRatio: number, maxRatio: number,
|
|
27
|
+
minMean: number, maxMean: number,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const getAggregatedColName = (aggF: string, colName: string): string => `${aggF}(${colName})`;
|
|
31
|
+
export type AggregationColumns = {[col: string]: DG.AggregationType};
|
|
32
|
+
|
|
14
33
|
export function getStats(data: RawData | number[], bitArray: BitArray): Stats {
|
|
15
34
|
if (data.length !== bitArray.length && data.some((v, i) => i >= bitArray.length ? v !== 0 : false))
|
|
16
35
|
throw new Error('PeptidesError: Data and bit array have different lengths');
|
|
@@ -29,12 +48,13 @@ export function getStats(data: RawData | number[], bitArray: BitArray): Stats {
|
|
|
29
48
|
rest[restIndex++] = data[i];
|
|
30
49
|
}
|
|
31
50
|
|
|
51
|
+
const selectedMean = selected.reduce((a, b) => a + b, 0) / selected.length;
|
|
32
52
|
if (selected.length === 1 || rest.length === 1) {
|
|
33
|
-
const selectedMean = selected.reduce((a, b) => a + b, 0) / selected.length;
|
|
34
53
|
const restMean = rest.reduce((a, b) => a + b, 0) / rest.length;
|
|
35
54
|
return {
|
|
36
55
|
count: selected.length,
|
|
37
56
|
pValue: null,
|
|
57
|
+
mean: selectedMean,
|
|
38
58
|
meanDifference: selectedMean - restMean,
|
|
39
59
|
ratio: selected.length / (bitArray.length),
|
|
40
60
|
mask: bitArray,
|
|
@@ -46,6 +66,7 @@ export function getStats(data: RawData | number[], bitArray: BitArray): Stats {
|
|
|
46
66
|
return {
|
|
47
67
|
count: selected.length,
|
|
48
68
|
pValue: testResult[currentMeanDiff >= 0 ? 'p-value more' : 'p-value less'],
|
|
69
|
+
mean: selectedMean,
|
|
49
70
|
meanDifference: currentMeanDiff,
|
|
50
71
|
ratio: selected.length / (bitArray.length),
|
|
51
72
|
mask: bitArray,
|
|
@@ -59,3 +80,19 @@ export function getAggregatedValue(col: DG.Column<number>, agg: DG.AggregationTy
|
|
|
59
80
|
//@ts-ignore: this is a hack to avoid using switch to access the getters
|
|
60
81
|
return stat[agg] as number;
|
|
61
82
|
}
|
|
83
|
+
|
|
84
|
+
export function getAggregatedColumnValues(df: DG.DataFrame, columns: AggregationColumns,
|
|
85
|
+
options: {filterDf?: boolean, mask?: DG.BitSet, fractionDigits?: number} = {}): StringDictionary {
|
|
86
|
+
options.filterDf ??= false;
|
|
87
|
+
options.fractionDigits ??= 3;
|
|
88
|
+
|
|
89
|
+
const filteredDf = options.filterDf && df.filter.anyFalse ? df.clone(df.filter) : df;
|
|
90
|
+
|
|
91
|
+
const colResults: StringDictionary = {};
|
|
92
|
+
for (const [colName, aggFn] of Object.entries(columns)) {
|
|
93
|
+
const newColName = getAggregatedColName(aggFn, colName);
|
|
94
|
+
const value = getAggregatedValue(filteredDf.getCol(colName), aggFn, options.mask);
|
|
95
|
+
colResults[newColName] = value.toFixed(options.fractionDigits);
|
|
96
|
+
}
|
|
97
|
+
return colResults;
|
|
98
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as ui from 'datagrok-api/ui';
|
|
2
|
+
import * as grok from 'datagrok-api/grok';
|
|
3
|
+
import * as DG from 'datagrok-api/dg';
|
|
4
|
+
|
|
5
|
+
import * as type from './types';
|
|
6
|
+
import * as C from '../utils/constants';
|
|
7
|
+
|
|
8
|
+
import {getActivityDistribution, getDistributionLegend, getStatsTableMap} from '../widgets/distribution';
|
|
9
|
+
import {getStatsSummary, prepareTableForHistogram} from '../utils/misc';
|
|
10
|
+
import {getMonomerWorksInstance} from '../package';
|
|
11
|
+
import {AggregationColumns, MonomerPositionStats, getAggregatedColumnValues} from './statistics';
|
|
12
|
+
|
|
13
|
+
export type TooltipOptions = {fromViewer?: boolean, isMutationCliffs?: boolean, x: number, y: number,
|
|
14
|
+
monomerPosition: type.SelectionItem, mpStats: MonomerPositionStats};
|
|
15
|
+
|
|
16
|
+
export function showMonomerTooltip(monomer: string, x: number, y: number): boolean {
|
|
17
|
+
const tooltipElements: HTMLDivElement[] = [];
|
|
18
|
+
const monomerName = monomer.toLowerCase();
|
|
19
|
+
|
|
20
|
+
const mw = getMonomerWorksInstance();
|
|
21
|
+
const mol = mw?.getCappedRotatedMonomer('PEPTIDE', monomer);
|
|
22
|
+
|
|
23
|
+
if (mol) {
|
|
24
|
+
tooltipElements.push(ui.div(monomerName));
|
|
25
|
+
const options = {autoCrop: true, autoCropMargin: 0, suppressChiralText: true};
|
|
26
|
+
tooltipElements.push(grok.chem.svgMol(mol, undefined, undefined, options));
|
|
27
|
+
} else if (monomer !== '')
|
|
28
|
+
tooltipElements.push(ui.div(monomer));
|
|
29
|
+
else
|
|
30
|
+
return true;
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
ui.tooltip.show(ui.divV(tooltipElements), x, y);
|
|
34
|
+
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function showTooltip(df: DG.DataFrame, columns: AggregationColumns, options: TooltipOptions): boolean {
|
|
39
|
+
options.fromViewer ??= false;
|
|
40
|
+
options.isMutationCliffs ??= false;
|
|
41
|
+
if (options.monomerPosition.positionOrClusterType === C.COLUMNS_NAMES.MONOMER)
|
|
42
|
+
showMonomerTooltip(options.monomerPosition.monomerOrCluster, options.x, options.y);
|
|
43
|
+
else
|
|
44
|
+
showTooltipAt(df, columns, options);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
//TODO: move out to viewer code
|
|
49
|
+
export function showTooltipAt(df: DG.DataFrame, columns: AggregationColumns, options: TooltipOptions): HTMLDivElement | null {
|
|
50
|
+
options.fromViewer ??= false;
|
|
51
|
+
options.isMutationCliffs ??= false;
|
|
52
|
+
const stats = options.mpStats[options.monomerPosition.positionOrClusterType]![options.monomerPosition.monomerOrCluster];
|
|
53
|
+
if (!stats?.count)
|
|
54
|
+
return null;
|
|
55
|
+
|
|
56
|
+
const activityCol = df.getCol(C.COLUMNS_NAMES.ACTIVITY_SCALED);
|
|
57
|
+
const mask = DG.BitSet.fromBytes(stats.mask.buffer.buffer, activityCol.length);
|
|
58
|
+
const distributionTable = DG.DataFrame.fromColumns(
|
|
59
|
+
[activityCol, DG.Column.fromBitSet(C.COLUMNS_NAMES.SPLIT_COL, mask)]);
|
|
60
|
+
const hist = getActivityDistribution(prepareTableForHistogram(distributionTable), true);
|
|
61
|
+
|
|
62
|
+
const tableMap = getStatsTableMap(stats);
|
|
63
|
+
if (options.fromViewer) {
|
|
64
|
+
tableMap['Mean difference'] = `${tableMap['Mean difference']}${options.isMutationCliffs ? ' (size)' : ''}`;
|
|
65
|
+
if (tableMap['p-value'])
|
|
66
|
+
tableMap['p-value'] = `${tableMap['p-value']}${options.isMutationCliffs ? ' (color)' : ''}`;
|
|
67
|
+
}
|
|
68
|
+
const aggregatedColMap = getAggregatedColumnValues(df, columns, {mask: mask});
|
|
69
|
+
const resultMap = {...tableMap, ...aggregatedColMap};
|
|
70
|
+
|
|
71
|
+
const labels = getDistributionLegend(
|
|
72
|
+
`${options.monomerPosition.positionOrClusterType} : ${options.monomerPosition.monomerOrCluster}`, 'Other');
|
|
73
|
+
const distroStatsElem = getStatsSummary(labels, hist, resultMap);
|
|
74
|
+
|
|
75
|
+
ui.tooltip.show(distroStatsElem, options.x, options.y);
|
|
76
|
+
|
|
77
|
+
return distroStatsElem;
|
|
78
|
+
}
|
package/src/utils/types.ts
CHANGED
|
@@ -50,3 +50,5 @@ export type StatsInfo = {
|
|
|
50
50
|
export type RawColumn = {name: string, rawData: RawData, cat?: string[]};
|
|
51
51
|
|
|
52
52
|
export type SelectionOptions = {shiftPressed: boolean, ctrlPressed: boolean};
|
|
53
|
+
|
|
54
|
+
export type CachedWebLogoTooltip = {bar: string, tooltip: HTMLDivElement | null};
|
|
@@ -7,7 +7,7 @@ import {CLUSTER_TYPE, ClusterType, PeptidesModel, VIEWER_TYPE} from '../model';
|
|
|
7
7
|
import * as C from '../utils/constants';
|
|
8
8
|
import * as CR from '../utils/cell-renderer';
|
|
9
9
|
import {HorizontalAlignments, IWebLogoViewer, PositionHeight} from '@datagrok-libraries/bio/src/viewers/web-logo';
|
|
10
|
-
import {getAggregatedValue, getStats, Stats} from '../utils/statistics';
|
|
10
|
+
import {getAggregatedColumnValues, getAggregatedValue, getStats, Stats} from '../utils/statistics';
|
|
11
11
|
import wu from 'wu';
|
|
12
12
|
import {getActivityDistribution, getDistributionLegend, getStatsTableMap} from '../widgets/distribution';
|
|
13
13
|
import {getStatsSummary, prepareTableForHistogram} from '../utils/misc';
|
|
@@ -319,7 +319,21 @@ export class LogoSummaryTable extends DG.JsViewer {
|
|
|
319
319
|
this.currentRowIndex = gridCell.gridRow;
|
|
320
320
|
}
|
|
321
321
|
});
|
|
322
|
-
this.viewerGrid.root.addEventListener('keydown', (ev) =>
|
|
322
|
+
this.viewerGrid.root.addEventListener('keydown', (ev) => {
|
|
323
|
+
this.keyPress = ev.key.startsWith('Arrow');
|
|
324
|
+
if (this.keyPress)
|
|
325
|
+
return;
|
|
326
|
+
if (ev.key === 'Escape' || (ev.code === 'KeyA' && ev.shiftKey && ev.ctrlKey))
|
|
327
|
+
this.model.initClusterSelection({notify: false});
|
|
328
|
+
else if (ev.code === 'KeyA' && ev.ctrlKey) {
|
|
329
|
+
for (let rowIdx = 0; rowIdx < summaryTable.rowCount; ++rowIdx) {
|
|
330
|
+
this.model.modifyClusterSelection(this.getCluster(this.viewerGrid.cell(C.LST_COLUMN_NAMES.CLUSTER, rowIdx)),
|
|
331
|
+
{shiftPressed: true, ctrlPressed: false}, false);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
this.model.fireBitsetChanged();
|
|
335
|
+
this.viewerGrid.invalidate();
|
|
336
|
+
});
|
|
323
337
|
this.viewerGrid.root.addEventListener('click', (ev) => {
|
|
324
338
|
const gridCell = this.viewerGrid.hitTest(ev.offsetX, ev.offsetY);
|
|
325
339
|
if (!gridCell || !gridCell.isTableCell || gridCell.tableColumn?.name !== C.LST_COLUMN_NAMES.CLUSTER)
|
|
@@ -449,7 +463,7 @@ export class LogoSummaryTable extends DG.JsViewer {
|
|
|
449
463
|
const activityColData = activityCol.getRawData();
|
|
450
464
|
|
|
451
465
|
if (cluster.positionOrClusterType === CLUSTER_TYPE.ORIGINAL) {
|
|
452
|
-
const origClustCol = filteredDf.getCol(
|
|
466
|
+
const origClustCol = filteredDf.getCol(this.model.settings.clustersColumnName!);
|
|
453
467
|
const origClustColData = origClustCol.getRawData();
|
|
454
468
|
const origClustColCategories = origClustCol.categories;
|
|
455
469
|
const seekValue = origClustColCategories.indexOf(cluster.monomerOrCluster);
|
|
@@ -475,7 +489,7 @@ export class LogoSummaryTable extends DG.JsViewer {
|
|
|
475
489
|
const labels = getDistributionLegend(`Cluster: ${cluster.monomerOrCluster}`, 'Other');
|
|
476
490
|
const hist = getActivityDistribution(distributionTable, true);
|
|
477
491
|
const tableMap = getStatsTableMap(stats);
|
|
478
|
-
const aggregatedColMap = this.model.
|
|
492
|
+
const aggregatedColMap = getAggregatedColumnValues(this.model.df, this.model.settings.columns!, {filterDf: true, mask: mask});
|
|
479
493
|
const resultMap: {[key: string]: any} = {...tableMap, ...aggregatedColMap};
|
|
480
494
|
const tooltip = getStatsSummary(labels, hist, resultMap);
|
|
481
495
|
|
|
@@ -5,11 +5,12 @@ import * as DG from 'datagrok-api/dg';
|
|
|
5
5
|
import $ from 'cash-dom';
|
|
6
6
|
import * as C from '../utils/constants';
|
|
7
7
|
import * as CR from '../utils/cell-renderer';
|
|
8
|
-
import {PeptidesModel,
|
|
8
|
+
import {PeptidesModel, VIEWER_TYPE} from '../model';
|
|
9
9
|
import wu from 'wu';
|
|
10
10
|
import {SelectionItem} from '../utils/types';
|
|
11
|
-
import {Stats} from '../utils/statistics';
|
|
11
|
+
import {PositionStats, Stats} from '../utils/statistics';
|
|
12
12
|
import {_package} from '../package';
|
|
13
|
+
import {showTooltip} from '../utils/tooltips';
|
|
13
14
|
|
|
14
15
|
export enum SELECTION_MODE {
|
|
15
16
|
MUTATION_CLIFFS = 'Mutation Cliffs',
|
|
@@ -75,7 +76,7 @@ export class MonomerPosition extends DG.JsViewer {
|
|
|
75
76
|
|
|
76
77
|
onTableAttached(): void {
|
|
77
78
|
super.onTableAttached();
|
|
78
|
-
this.helpUrl = '/help/domains/bio/peptides
|
|
79
|
+
this.helpUrl = 'https://datagrok.ai/help/datagrok/solutions/domains/bio/peptides-sar';
|
|
79
80
|
this.render();
|
|
80
81
|
}
|
|
81
82
|
|
|
@@ -125,7 +126,9 @@ export class MonomerPosition extends DG.JsViewer {
|
|
|
125
126
|
}
|
|
126
127
|
const monomerPosition = this.getMonomerPosition(gridCell);
|
|
127
128
|
this.model.highlightMonomerPosition(monomerPosition);
|
|
128
|
-
return this.model.
|
|
129
|
+
return showTooltip(this.model.df, this.model.settings.columns!, {fromViewer: true,
|
|
130
|
+
isMutationCliffs: this.mode === SELECTION_MODE.MUTATION_CLIFFS, monomerPosition, x, y,
|
|
131
|
+
mpStats: this.model.monomerPositionStats});
|
|
129
132
|
});
|
|
130
133
|
this.viewerGrid.root.addEventListener('mouseleave', (_ev) => this.model.unhighlight());
|
|
131
134
|
DG.debounce(this.viewerGrid.onCurrentCellChanged, 500).subscribe((gridCell: DG.GridCell) => {
|
|
@@ -151,7 +154,31 @@ export class MonomerPosition extends DG.JsViewer {
|
|
|
151
154
|
this.currentGridCell = gridCell;
|
|
152
155
|
}
|
|
153
156
|
});
|
|
154
|
-
this.viewerGrid.root.addEventListener('keydown', (ev) =>
|
|
157
|
+
this.viewerGrid.root.addEventListener('keydown', (ev) => {
|
|
158
|
+
this.keyPressed = ev.key.startsWith('Arrow');
|
|
159
|
+
if (this.keyPressed)
|
|
160
|
+
return;
|
|
161
|
+
if (ev.key === 'Escape' || (ev.code === 'KeyA' && ev.ctrlKey && ev.shiftKey)) {
|
|
162
|
+
if (this.mode === SELECTION_MODE.INVARIANT_MAP)
|
|
163
|
+
this.model.initInvariantMapSelection({notify: false});
|
|
164
|
+
else
|
|
165
|
+
this.model.initMutationCliffsSelection({notify: false});
|
|
166
|
+
} else if (ev.code === 'KeyA' && ev.ctrlKey) {
|
|
167
|
+
const positions = Object.keys(this.model.monomerPositionStats).filter((pos) => pos !== 'general');
|
|
168
|
+
for (const position of positions) {
|
|
169
|
+
const monomers = Object.keys(this.model.monomerPositionStats[position]!).filter((monomer) => monomer !== 'general');
|
|
170
|
+
for (const monomer of monomers) {
|
|
171
|
+
const monomerPosition = {monomerOrCluster: monomer, positionOrClusterType: position};
|
|
172
|
+
if (this.mode === SELECTION_MODE.INVARIANT_MAP)
|
|
173
|
+
this.model.modifyInvariantMapSelection(monomerPosition, {shiftPressed: true, ctrlPressed: false}, false);
|
|
174
|
+
else
|
|
175
|
+
this.model.modifyMutationCliffsSelection(monomerPosition, {shiftPressed: true, ctrlPressed: false}, false);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
this.model.fireBitsetChanged();
|
|
180
|
+
this.viewerGrid.invalidate();
|
|
181
|
+
});
|
|
155
182
|
this.viewerGrid.root.addEventListener('click', (ev) => {
|
|
156
183
|
const gridCell = this.viewerGrid.hitTest(ev.offsetX, ev.offsetY);
|
|
157
184
|
if (!gridCell?.isTableCell || gridCell?.tableColumn?.name === C.COLUMNS_NAMES.MONOMER)
|
|
@@ -257,7 +284,7 @@ export class MostPotentResidues extends DG.JsViewer {
|
|
|
257
284
|
|
|
258
285
|
onTableAttached(): void {
|
|
259
286
|
super.onTableAttached();
|
|
260
|
-
this.helpUrl = '/help/domains/bio/peptides
|
|
287
|
+
this.helpUrl = 'https://datagrok.ai/help/datagrok/solutions/domains/bio/peptides-sar';
|
|
261
288
|
this.render();
|
|
262
289
|
}
|
|
263
290
|
|
|
@@ -274,6 +301,7 @@ export class MostPotentResidues extends DG.JsViewer {
|
|
|
274
301
|
const pValData: (number | null)[] = new Array(posData.length);
|
|
275
302
|
const countData: number[] = new Array(posData.length);
|
|
276
303
|
const ratioData: number[] = new Array(posData.length);
|
|
304
|
+
const meanData: number[] = new Array(posData.length);
|
|
277
305
|
|
|
278
306
|
let i = 0;
|
|
279
307
|
for (const [position, positionStats] of monomerPositionStatsEntries) {
|
|
@@ -312,6 +340,7 @@ export class MostPotentResidues extends DG.JsViewer {
|
|
|
312
340
|
pValData[i] = maxEntry![1].pValue;
|
|
313
341
|
countData[i] = maxEntry![1].count;
|
|
314
342
|
ratioData[i] = maxEntry![1].ratio;
|
|
343
|
+
meanData[i] = maxEntry![1].mean;
|
|
315
344
|
++i;
|
|
316
345
|
}
|
|
317
346
|
|
|
@@ -327,6 +356,7 @@ export class MostPotentResidues extends DG.JsViewer {
|
|
|
327
356
|
mprDfCols.add(DG.Column.fromList(DG.TYPE.INT, C.COLUMNS_NAMES.POSITION, posData));
|
|
328
357
|
mprDfCols.add(DG.Column.fromList(DG.TYPE.STRING, C.COLUMNS_NAMES.MONOMER, monomerData));
|
|
329
358
|
mprDfCols.add(DG.Column.fromList(DG.TYPE.FLOAT, C.COLUMNS_NAMES.MEAN_DIFFERENCE, mdData));
|
|
359
|
+
mprDfCols.add(DG.Column.fromList(DG.TYPE.FLOAT, C.COLUMNS_NAMES.MEAN, meanData));
|
|
330
360
|
mprDfCols.add(DG.Column.fromList(DG.TYPE.FLOAT, C.COLUMNS_NAMES.P_VALUE, pValData));
|
|
331
361
|
mprDfCols.add(DG.Column.fromList(DG.TYPE.INT, C.COLUMNS_NAMES.COUNT, countData));
|
|
332
362
|
mprDfCols.add(DG.Column.fromList(DG.TYPE.FLOAT, C.COLUMNS_NAMES.RATIO, ratioData));
|
|
@@ -359,7 +389,8 @@ export class MostPotentResidues extends DG.JsViewer {
|
|
|
359
389
|
monomerPosition.positionOrClusterType = C.COLUMNS_NAMES.MONOMER;
|
|
360
390
|
else if (gridCell.tableColumn?.name !== C.COLUMNS_NAMES.MEAN_DIFFERENCE)
|
|
361
391
|
return false;
|
|
362
|
-
return this.model.
|
|
392
|
+
return showTooltip(this.model.df, this.model.settings.columns!,
|
|
393
|
+
{fromViewer: true, isMutationCliffs: true, monomerPosition, x, y, mpStats: this.model.monomerPositionStats});
|
|
363
394
|
});
|
|
364
395
|
DG.debounce(this.viewerGrid.onCurrentCellChanged, 500).subscribe((gridCell: DG.GridCell) => {
|
|
365
396
|
try {
|
|
@@ -378,7 +409,21 @@ export class MostPotentResidues extends DG.JsViewer {
|
|
|
378
409
|
this.currentGridRowIdx = gridCell.gridRow;
|
|
379
410
|
}
|
|
380
411
|
});
|
|
381
|
-
this.viewerGrid.root.addEventListener('keydown', (ev) =>
|
|
412
|
+
this.viewerGrid.root.addEventListener('keydown', (ev) => {
|
|
413
|
+
this.keyPressed = ev.key.startsWith('Arrow');
|
|
414
|
+
if (this.keyPressed)
|
|
415
|
+
return;
|
|
416
|
+
if (ev.key === 'Escape' || (ev.code === 'KeyA' && ev.ctrlKey && ev.shiftKey))
|
|
417
|
+
this.model.initMutationCliffsSelection({notify: false});
|
|
418
|
+
else if (ev.code === 'KeyA' && ev.ctrlKey) {
|
|
419
|
+
for (let rowIdx = 0; rowIdx < mostPotentResiduesDf.rowCount; ++rowIdx) {
|
|
420
|
+
const monomerPosition = this.getMonomerPosition(this.viewerGrid.cell('Diff', rowIdx));
|
|
421
|
+
this.model.modifyMutationCliffsSelection(monomerPosition, {shiftPressed: true, ctrlPressed: false}, false);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
this.model.fireBitsetChanged();
|
|
425
|
+
this.viewerGrid.invalidate();
|
|
426
|
+
});
|
|
382
427
|
this.viewerGrid.root.addEventListener('mouseleave', (_ev) => this.model.unhighlight());
|
|
383
428
|
this.viewerGrid.root.addEventListener('click', (ev) => {
|
|
384
429
|
const gridCell = this.viewerGrid.hitTest(ev.offsetX, ev.offsetY);
|
|
@@ -5,7 +5,7 @@ import {StringDictionary} from '@datagrok-libraries/utils/src/type-declarations'
|
|
|
5
5
|
import $ from 'cash-dom';
|
|
6
6
|
|
|
7
7
|
import * as C from '../utils/constants';
|
|
8
|
-
import {getStats, Stats} from '../utils/statistics';
|
|
8
|
+
import {getAggregatedColumnValues, getStats, Stats} from '../utils/statistics';
|
|
9
9
|
import {PeptidesModel} from '../model';
|
|
10
10
|
import {getStatsSummary, prepareTableForHistogram} from '../utils/misc';
|
|
11
11
|
import BitArray from '@datagrok-libraries/utils/src/bit-array';
|
|
@@ -57,7 +57,7 @@ export function getDistributionWidget(table: DG.DataFrame, model: PeptidesModel)
|
|
|
57
57
|
const stats = model.monomerPositionStats[position]![monomer]!;
|
|
58
58
|
const tableMap = getStatsTableMap(stats);
|
|
59
59
|
|
|
60
|
-
const aggregatedColMap = model.
|
|
60
|
+
const aggregatedColMap = getAggregatedColumnValues(model.df, model.settings.columns!, {filterDf: true, mask});
|
|
61
61
|
|
|
62
62
|
const resultMap = {...tableMap, ...aggregatedColMap};
|
|
63
63
|
const distributionRoot = getStatsSummary(labels, hist, resultMap);
|
|
@@ -83,7 +83,7 @@ export function getDistributionWidget(table: DG.DataFrame, model: PeptidesModel)
|
|
|
83
83
|
const mask = DG.BitSet.create(rowCount, (i) => monomerIndexesList.includes(posColData[i]));
|
|
84
84
|
const splitCol = DG.Column.fromBitSet(C.COLUMNS_NAMES.SPLIT_COL, mask);
|
|
85
85
|
|
|
86
|
-
const aggregatedColMap = model.
|
|
86
|
+
const aggregatedColMap = getAggregatedColumnValues(model.df, model.settings.columns!, {filterDf: true, mask});
|
|
87
87
|
|
|
88
88
|
const distributionTable = DG.DataFrame.fromColumns([activityCol, splitCol]);
|
|
89
89
|
const hist = getActivityDistribution(prepareTableForHistogram(distributionTable));
|
|
@@ -126,7 +126,7 @@ export function getDistributionWidget(table: DG.DataFrame, model: PeptidesModel)
|
|
|
126
126
|
|
|
127
127
|
const mask = DG.BitSet.create(rowCount,
|
|
128
128
|
(i) => posColDataList.some((posColData, j) => posColData[i] === monomerCategoryIndexList[j]));
|
|
129
|
-
const aggregatedColMap = model.
|
|
129
|
+
const aggregatedColMap = getAggregatedColumnValues(model.df, model.settings.columns!, {filterDf: true, mask});
|
|
130
130
|
|
|
131
131
|
const splitCol = DG.Column.fromBitSet(C.COLUMNS_NAMES.SPLIT_COL, mask);
|
|
132
132
|
const distributionTable = DG.DataFrame.fromColumns([activityCol, splitCol]);
|
|
@@ -166,8 +166,10 @@ export function getDistributionWidget(table: DG.DataFrame, model: PeptidesModel)
|
|
|
166
166
|
const bitArray = BitArray.fromString(table.selection.toBinaryString());
|
|
167
167
|
const mask = DG.BitSet.create(rowCount,
|
|
168
168
|
bitArray.allFalse || bitArray.allTrue ? (_): boolean => true : (i): boolean => bitArray.getBit(i));
|
|
169
|
-
const aggregatedColMap = model.
|
|
170
|
-
const stats = bitArray.allFalse || bitArray.allTrue ?
|
|
169
|
+
const aggregatedColMap = getAggregatedColumnValues(model.df, model.settings.columns!, {filterDf: true, mask});
|
|
170
|
+
const stats = bitArray.allFalse || bitArray.allTrue ?
|
|
171
|
+
{count: rowCount, pValue: null, meanDifference: 0, ratio: 1, mask: bitArray,
|
|
172
|
+
mean: activityCol.stats.avg} :
|
|
171
173
|
getStats(activityColData, bitArray);
|
|
172
174
|
const tableMap = getStatsTableMap(stats);
|
|
173
175
|
const resultMap: {[key: string]: any} = {...tableMap, ...aggregatedColMap};
|
|
@@ -231,6 +233,7 @@ export function getStatsTableMap(stats: Stats, options: {fractionDigits?: number
|
|
|
231
233
|
const tableMap: StringDictionary = {
|
|
232
234
|
'Count': `${stats.count} (${stats.ratio.toFixed(options.fractionDigits)}%)`,
|
|
233
235
|
'Mean difference': stats.meanDifference.toFixed(options.fractionDigits),
|
|
236
|
+
'Mean activity': stats.mean.toFixed(options.fractionDigits),
|
|
234
237
|
};
|
|
235
238
|
if (stats.pValue !== null)
|
|
236
239
|
tableMap['p-value'] = stats.pValue < 0.01 ? '<0.01' : stats.pValue.toFixed(options.fractionDigits);
|
package/src/widgets/selection.ts
CHANGED
|
@@ -6,6 +6,10 @@ import {PeptidesModel} from '../model';
|
|
|
6
6
|
import wu from 'wu';
|
|
7
7
|
import {COLUMNS_NAMES} from '../utils/constants';
|
|
8
8
|
import {addExpandIcon} from '../utils/misc';
|
|
9
|
+
import {CellRendererOptions, WebLogoBounds, setWebLogoRenderer} from '../utils/cell-renderer';
|
|
10
|
+
import {CachedWebLogoTooltip, SelectionItem} from '../utils/types';
|
|
11
|
+
import {TooltipOptions} from '../utils/tooltips';
|
|
12
|
+
import {calculateMonomerPositionStatistics} from '../utils/algorithms';
|
|
9
13
|
|
|
10
14
|
export function getSelectionWidget(table: DG.DataFrame, model: PeptidesModel): HTMLElement {
|
|
11
15
|
const compBitset = model.getCompoundBitset();
|
|
@@ -58,5 +62,15 @@ export function getSelectionWidget(table: DG.DataFrame, model: PeptidesModel): H
|
|
|
58
62
|
grid.col(gridCol.name)!.width = gridCol.width;
|
|
59
63
|
}
|
|
60
64
|
}, 500);
|
|
65
|
+
|
|
66
|
+
const mpStats = calculateMonomerPositionStatistics(grid.dataFrame, model.positionColumns.toArray());
|
|
67
|
+
|
|
68
|
+
const cachedWebLogoTooltip: CachedWebLogoTooltip = {bar: '', tooltip: null};
|
|
69
|
+
const webLogoBounds: WebLogoBounds = {};
|
|
70
|
+
const cellRendererOptions: CellRendererOptions = {isSelectionTable: true, cachedWebLogoTooltip, webLogoBounds};
|
|
71
|
+
const tooltipOptions: TooltipOptions = {x: 0, y: 0, monomerPosition: {} as SelectionItem, mpStats};
|
|
72
|
+
|
|
73
|
+
setWebLogoRenderer(grid, model, cellRendererOptions, tooltipOptions);
|
|
74
|
+
|
|
61
75
|
return gridHost;
|
|
62
76
|
}
|