@datagrok/peptides 0.8.7 → 0.8.10

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.
@@ -1,409 +1,398 @@
1
1
  import * as DG from 'datagrok-api/dg';
2
- import * as ui from 'datagrok-api/ui';
3
- import {scaleBand, scaleLinear} from 'd3';
4
- import {ChemPalette} from '../utils/chem-palette';
5
2
  import * as rxjs from 'rxjs';
6
- const cp = new ChemPalette('grok');
7
-
8
- //TODO: the function should not accept promise. Await the parameters where it is used
9
- export function addViewerToHeader(grid: DG.Grid, viewer: Promise<DG.Widget>) {
10
- viewer.then((viewer) => {
11
- const barchart = viewer as StackedBarChart; //TODO: accept specifically StackedBarChart object
12
- // The following event makes the barchart interactive
13
- rxjs.fromEvent(grid.overlay, 'mousemove').subscribe((mm:any) => {
14
- mm = mm as MouseEvent;
15
- const cell = grid.hitTest(mm.offsetX, mm.offsetY);
16
- if (cell !== null && cell?.isColHeader && cell.tableColumn?.semType == 'aminoAcids')
17
- barchart.highlight(cell, mm.offsetX, mm.offsetY);
18
- });
19
-
20
- rxjs.fromEvent(grid.overlay, 'click').subscribe((mm:any) => {
21
- mm = mm as MouseEvent;
3
+ import * as ui from 'datagrok-api/ui';
4
+ import {MonomerLibrary} from '../monomer-library';
5
+ import {PeptidesController} from '../peptides';
6
+
7
+ export function addViewerToHeader(grid: DG.Grid, barchart: StackedBarChart) {
8
+ if (grid.temp['containsBarchart'])
9
+ return;
10
+
11
+ function eventAction(mouseMove: MouseEvent) {
12
+ const cell = grid.hitTest(mouseMove.offsetX, mouseMove.offsetY);
13
+ if (cell !== null && cell?.isColHeader && cell.tableColumn?.semType == 'aminoAcids')
14
+ barchart.highlight(cell, mouseMove.offsetX, mouseMove.offsetY, mouseMove);
15
+ else
16
+ return;
22
17
 
23
- const cell = grid.hitTest(mm.offsetX, mm.offsetY);
24
- if (cell?.isColHeader && cell.tableColumn?.semType == 'aminoAcids') {
25
- barchart.beginSelection(mm);
26
- return;
27
- }
28
- barchart.unhighlight();
29
- });
30
- rxjs.fromEvent(grid.overlay, 'mouseout').subscribe((_: any) => {
18
+ if (cell?.isColHeader && cell.tableColumn?.semType == 'aminoAcids')
19
+ barchart.beginSelection(mouseMove);
20
+ else
31
21
  barchart.unhighlight();
32
- });
22
+ }
33
23
 
34
- barchart.tableCanvas = grid.canvas;
35
- grid.setOptions({'colHeaderHeight': 200});
36
- grid.onCellTooltip((cell, x, y) => {
37
- if (cell.tableColumn) {
38
- if (['aminoAcids', 'alignedSequence'].includes(cell.tableColumn.semType) ) {
39
- if ( !cell.isColHeader) {
40
- cp.showTooltip(cell, x, y);
41
- return true;
42
- } else {
43
- if (barchart.highlighted) {
44
- let elements: HTMLElement[] = [];
45
- elements = elements.concat([ui.divText(barchart.highlighted.aaName)]);
46
- ui.tooltip.show(ui.divV(elements), x, y);
47
- }
48
- return true;
49
- }
24
+ // The following events makes the barchart interactive
25
+ rxjs.fromEvent<MouseEvent>(grid.overlay, 'mousemove').subscribe((mouseMove: MouseEvent) => eventAction(mouseMove));
26
+ rxjs.fromEvent<MouseEvent>(grid.overlay, 'click').subscribe((mouseMove: MouseEvent) => eventAction(mouseMove));
27
+ rxjs.fromEvent<MouseEvent>(grid.overlay, 'mouseout').subscribe(() => barchart.unhighlight());
28
+
29
+ barchart.tableCanvas = grid.canvas;
30
+
31
+ //Setting grid options
32
+ grid.setOptions({'colHeaderHeight': 130});
33
+
34
+ grid.onCellTooltip((cell, x, y) => {
35
+ if (cell.tableColumn && ['aminoAcids', 'alignedSequence'].includes(cell.tableColumn.semType) ) {
36
+ if (!cell.isColHeader) {
37
+ const monomerLib = cell.cell.dataFrame.temp[MonomerLibrary.id];
38
+ PeptidesController.chemPalette.showTooltip(cell, x, y, monomerLib);
39
+ } else {
40
+ if (barchart.highlighted) {
41
+ let elements: HTMLElement[] = [];
42
+ elements = elements.concat([ui.divText(barchart.highlighted.aaName)]);
43
+ ui.tooltip.show(ui.divV(elements), x, y);
50
44
  }
51
45
  }
52
- });
53
- grid.onCellRender.subscribe((args) => {
54
- args.g.save();
55
- args.g.beginPath();
56
- args.g.rect(args.bounds.x, args.bounds.y, args.bounds.width, args.bounds.height);
57
- args.g.clip();
58
-
59
- if (args.cell.isColHeader && barchart.aminoColumnNames.includes(args.cell.gridColumn.name)) {
60
- barchart.renderBarToCanvas(
61
- args.g,
62
- args.cell,
63
- args.bounds.x,
64
- args.bounds.y,
65
- args.bounds.width,
66
- args.bounds.height,
67
- );
68
- args.preventDefault();
69
- }
70
- args.g.restore();
71
- });
46
+ }
47
+ return true;
72
48
  });
49
+
50
+ grid.onCellRender.subscribe((args) => {
51
+ const context = args.g;
52
+ const boundX = args.bounds.x;
53
+ const boundY = args.bounds.y;
54
+ const boundWidth = args.bounds.width;
55
+ const boundHeight = args.bounds.height;
56
+ const cell = args.cell;
57
+ context.save();
58
+ context.beginPath();
59
+ context.rect(boundX, boundY, boundWidth, boundHeight);
60
+ context.clip();
61
+
62
+ if (cell.isColHeader && barchart.aminoColumnNames.includes(cell.gridColumn.name)) {
63
+ barchart.renderBarToCanvas(context, cell, boundX, boundY, boundWidth, boundHeight);
64
+ args.preventDefault();
65
+ }
66
+ context.restore();
67
+ });
68
+
69
+ grid.temp['containsBarchart'] = true;
70
+ //FIXME: for some reason barchat didn't show when running analysis. This fixes it, but it's bad. Find a way to fix
71
+ // the problem
72
+ barchart.unhighlight();
73
73
  }
74
74
 
75
+ type stackedBarChartDatatype = {
76
+ 'name': string,
77
+ 'data': {'name': string, 'count': number, 'selectedCount': number, 'fixedSelectedCount': number}[],
78
+ }[];
79
+
80
+ type bartStatsType = {
81
+ [Key: string]: {'name': string, 'count': number, 'selectedCount': number, 'fixedSelectedCount': number}[],
82
+ };
75
83
 
76
84
  export class StackedBarChart extends DG.JsViewer {
77
- public dataEmptyAA: string;
78
- public initialized: boolean;
79
- highlighted: {'colName' : string, 'aaName' : string} | null = null;
80
- private ord: { [Key: string]: number; } = {};
81
- private margin: {top: number; left: number; bottom: number; right: number} = {
82
- top: 10,
83
- right: 10,
84
- bottom: 50,
85
- left: 10,
85
+ public dataEmptyAA: string;
86
+ public highlighted: {'colName' : string, 'aaName' : string} | null = null;
87
+ public tableCanvas: HTMLCanvasElement | undefined;
88
+ public aminoColumnNames: string[] = [];
89
+ private ord: { [Key: string]: number; } = {};
90
+ private aminoColumnIndices: {[Key: string]: number} = {};
91
+ private aggregatedTables: {[Key: string]: DG.DataFrame} = {};
92
+ private aggregatedHighlightedTables: {[Key: string]: DG.DataFrame} = {};
93
+ private max = 0;
94
+ private barStats:
95
+ {[Key: string]: {'name': string, 'count': number, 'highlightedCount': number, 'selectedCount': number}[]} = {};
96
+ private selected: {'colName' : string, 'aaName' : string}[] = [];
97
+ private highlightedMask: DG.BitSet | null = null;
98
+ private aggregatedSelectedTables: {[Key: string]: DG.DataFrame} = {};
99
+
100
+ constructor() {
101
+ super();
102
+ this.dataEmptyAA = this.string('dataEmptyAA', '-');
103
+ }
104
+
105
+ init() {
106
+ const groups: {[key: string]: string[]} = {
107
+ 'yellow': ['C', 'U'],
108
+ 'red': ['G', 'P'],
109
+ 'all_green': ['A', 'V', 'I', 'L', 'M', 'F', 'Y', 'W'],
110
+ 'light_blue': ['R', 'H', 'K'],
111
+ 'dark_blue': ['D', 'E'],
112
+ 'orange': ['S', 'T', 'N', 'Q'],
86
113
  };
87
- private yScale: any;
88
- private xScale: any;
89
- private data: {'name': string, 'data': {'name': string, 'count': number, 'selectedCount': number}[]}[] = [];
90
- private selectionMode: boolean = false;
91
- public aminoColumnNames: string[] = [];
92
-
93
- private aminoColumnIndices: {[Key: string]: number} = {};
94
- private aggregatedTables: {[Key: string]: DG.DataFrame} = {};
95
- private aggregatedTablesUnselected: {[Key: string]: DG.DataFrame} = {};
96
- private max = 0;
97
- private barStats: {[Key: string]: {'name': string, 'count': number, 'selectedCount': number}[]} = {};
98
- tableCanvas: HTMLCanvasElement | undefined;
99
- private registered: {[Key: string]: DG.GridCell} = {};
100
-
101
- constructor() {
102
- super();
103
- this.dataEmptyAA = this.string('dataEmptyAA', '-');
104
- this.initialized = false;
114
+ let i = 0;
115
+
116
+ for (const value of Object.values(groups)) {
117
+ for (const obj of value)
118
+ this.ord[obj] = i++;
105
119
  }
106
120
 
107
- init() {
108
- const groups: {[key: string]: string[]} = {
109
- 'yellow': ['C', 'U'],
110
- 'red': ['G', 'P'],
111
- 'all_green': ['A', 'V', 'I', 'L', 'M', 'F', 'Y', 'W'],
112
- 'light_blue': ['R', 'H', 'K'],
113
- 'dark_blue': ['D', 'E'],
114
- 'orange': ['S', 'T', 'N', 'Q'],
115
- };
116
-
117
- let i = 0;
118
- for (const value of Object.values(groups)) {
119
- i++;
120
- for (const obj of value)
121
- this.ord[obj] = i;
122
- }
123
- this.yScale = scaleLinear();
124
- this.xScale = scaleBand();
125
- this.data = [];
121
+ this.aminoColumnNames = [];
122
+ }
126
123
 
127
- this.aminoColumnNames = [];
124
+ // Stream subscriptions
125
+ onTableAttached() {
126
+ this.init();
127
+ if (this.dataFrame) {
128
+ this.subs.push(DG.debounce(this.dataFrame.selection.onChanged, 50).subscribe((_) => this.computeData()));
129
+ this.subs.push(DG.debounce(this.dataFrame.filter.onChanged, 50).subscribe((_) => this.computeData()));
130
+ this.highlightedMask = DG.BitSet.create(this.dataFrame.rowCount);
128
131
  }
129
-
130
- // Stream subscriptions
131
- onTableAttached() {
132
- this.init();
133
- if (this.dataFrame) {
134
- this.subs.push(DG.debounce(this.dataFrame.selection.onChanged, 50).subscribe((_) => this.render()));
135
- this.subs.push(DG.debounce(this.dataFrame.filter.onChanged, 50).subscribe((_) => this.render()));
136
- this.subs.push(DG.debounce(this.dataFrame.onCurrentRowChanged, 50).subscribe((_) => this.render()));
137
- this.subs.push(DG.debounce(ui.onSizeChanged(this.root), 50).subscribe((_) => this.render(false)));
138
- this.computeData(this.dataFrame);
132
+ }
133
+
134
+ // Cancel subscriptions when the viewer is detached
135
+ detach() {
136
+ this.subs.forEach((sub) => sub.unsubscribe());
137
+ }
138
+
139
+ computeData() {
140
+ this.aminoColumnNames = [];
141
+ this.aminoColumnIndices = {};
142
+
143
+ this.dataFrame!.columns.names().forEach((name: string) => {
144
+ if (this.dataFrame!.getCol(name).semType === 'aminoAcids' &&
145
+ !this.dataFrame!.getCol(name).categories.includes('COOH') &&
146
+ !this.dataFrame!.getCol(name).categories.includes('NH2')) {
147
+ this.aminoColumnIndices[name] = this.aminoColumnNames.length + 1;
148
+ this.aminoColumnNames.push(name);
139
149
  }
140
- }
150
+ });
141
151
 
142
- // Cancel subscriptions when the viewer is detached
143
- detach() {
144
- this.subs.forEach((sub) => sub.unsubscribe());
145
- }
152
+ this.aggregatedTables = {};
153
+ this.aggregatedHighlightedTables = {};
154
+ this.aggregatedSelectedTables = {};
155
+ //TODO: optimize it, why store so many tables?
156
+ this.aminoColumnNames.forEach((name) => {
157
+ this.aggregatedTables[name] = this.dataFrame!
158
+ .groupBy([name])
159
+ .whereRowMask(this.dataFrame!.filter)
160
+ .add('count', name, `${name}_count`)
161
+ .aggregate();
162
+
163
+ this.aggregatedHighlightedTables[name] = this.dataFrame!
164
+ .groupBy([name])
165
+ .whereRowMask(this.highlightedMask!)
166
+ .add('count', name, `${name}_count`)
167
+ .aggregate();
168
+
169
+ this.aggregatedSelectedTables[name] = this.dataFrame!
170
+ .groupBy([name])
171
+ .whereRowMask(this.dataFrame!.selection)
172
+ .add('count', name, `${name}_count`)
173
+ .aggregate();
174
+ });
146
175
 
147
- computeData(df: DG.DataFrame) {
148
- this.data = [];
149
- this.aminoColumnNames = [];
150
- this.aminoColumnIndices = {};
151
-
152
- df.columns.names().forEach((name: string) => {
153
- {
154
- // @ts-ignore
155
- if (df.getCol(name).semType === 'aminoAcids' &&
156
- !df.getCol(name).categories.includes('COOH') &&
157
- !df.getCol(name).categories.includes('NH2')) {
158
- this.aminoColumnIndices[name] = this.aminoColumnNames.length + 1;
159
- this.aminoColumnNames.push(name);
160
- }
161
- }
162
- });
163
-
164
- this.aggregatedTables = {};
165
- this.aggregatedTablesUnselected = {};
166
- const buf1 = df.selection.getBuffer();
167
- const buf2 = df.filter.getBuffer();
168
- const resbuf = new Int32Array(df.rowCount);
169
-
170
- for (let i = 0; i < buf2.length; i++)
171
- resbuf[i] = buf1[i] & buf2[i];
172
-
173
-
174
- //TODO: optimize it, why store so many tables?
175
- const mask = DG.BitSet.fromBytes(resbuf.buffer, df.rowCount);
176
- if (mask.trueCount !== df.filter.trueCount) {
177
- this.selectionMode = true;
178
- this.aminoColumnNames.forEach((name) => {
179
- this.aggregatedTables[name] = df
180
- .groupBy([name])
181
- .whereRowMask(df.filter)
182
- .add('count', name, `${name}_count`)
183
- .aggregate();
184
- const buf1 = df.selection.getBuffer();
185
- const buf2 = df.filter.getBuffer();
186
- const resbuf = new Int32Array(df.rowCount);
187
-
188
- for (let i = 0; i < buf2.length; i++)
189
- resbuf[i] = buf1[i] & buf2[i];
190
-
191
-
192
- // @ts-ignore
193
- const mask = DG.BitSet.fromBytes(resbuf.buffer, df.rowCount);
194
- // @ts-ignore
195
- this.aggregatedTablesUnselected[name] = df
196
- .groupBy([name])
197
- .whereRowMask(mask)
198
- .add('count', name, `${name}_count`)
199
- .aggregate();
200
- });
201
- } else {
202
- this.selectionMode = false;
203
- this.aminoColumnNames.forEach((name) => {
204
- // @ts-ignore
205
- this.aggregatedTables[name] = df
206
- .groupBy([name])
207
- .whereRowMask(df.filter)
208
- .add('count', name, `${name}_count`)
209
- .aggregate();
210
- },
211
- );
212
- }
213
- this.data = [];
214
- this.barStats = {};
215
- for (const [name, df] of Object.entries(this.aggregatedTables)) {
216
- const colObj: {
217
- 'name': string,
218
- 'data': { 'name': string, 'count': number, 'selectedCount': number }[],
219
- } = {'name': name, 'data': []};
220
- this.barStats[colObj['name']] = colObj['data'];
221
- this.data.push(colObj);
222
- for (let i = 0; i < df.rowCount; i++) {
223
- const amino = df.getCol(name).get(i);
224
- const aminoCount = df.getCol(`${name}_count`).get(i);
225
- if ((!amino) || amino === this.dataEmptyAA)
226
- continue;
227
-
228
- const aminoObj = {'name': amino, 'count': aminoCount, 'selectedCount': 0};
229
- colObj['data'].push(aminoObj);
230
- for (let j = 0; j < this.aggregatedTablesUnselected[name].rowCount; j++) {
231
- const unsAmino = this.aggregatedTablesUnselected[name].getCol(`${name}`).get(j);
232
- if (unsAmino == amino) {
233
- aminoObj['selectedCount'] = this.aggregatedTablesUnselected[name]
234
- .getCol(`${name}_count`)
235
- .get(j);
176
+ this.barStats = {};
177
+
178
+ for (const [name, df] of Object.entries(this.aggregatedTables)) {
179
+ const colObj: {
180
+ 'name': string,
181
+ 'data': { 'name': string, 'count': number, 'highlightedCount': number, 'selectedCount': number}[],
182
+ } = {'name': name, 'data': []};
183
+ const aminoCol = df.getCol(name);
184
+ const aminoCountCol = df.getCol(`${name}_count`);
185
+ this.barStats[colObj['name']] = colObj['data'];
186
+
187
+ for (let i = 0; i < df.rowCount; i++) {
188
+ const amino = aminoCol.get(i);
189
+ const aminoCount = aminoCountCol.get(i);
190
+ const aminoObj = {'name': amino, 'count': aminoCount, 'highlightedCount': 0, 'selectedCount': 0};
191
+ const aggHighlightedAminoCol = this.aggregatedHighlightedTables[name].getCol(`${name}`);
192
+ const aggHighlightedCountCol = this.aggregatedHighlightedTables[name].getCol(`${name}_count`);
193
+ const aggSelectedAminoCol = this.aggregatedSelectedTables[name].getCol(`${name}`);
194
+ const aggSelectedCountCol = this.aggregatedSelectedTables[name].getCol(`${name}_count`);
195
+
196
+ if (!amino || amino === this.dataEmptyAA)
197
+ continue;
198
+
199
+ colObj['data'].push(aminoObj);
200
+
201
+ for (const col of [aggHighlightedCountCol, aggSelectedCountCol]) {
202
+ for (let j = 0; j < col.length; j++) {
203
+ const highlightedAmino = aggHighlightedAminoCol.get(j);
204
+ const selectedAmino = aggSelectedAminoCol.get(j);
205
+ const curAmino = (col == aggHighlightedCountCol ? highlightedAmino : selectedAmino);
206
+ if (curAmino == amino) {
207
+ aminoObj[col == aggHighlightedCountCol ? 'highlightedCount' : 'selectedCount'] = col.get(j);
236
208
  break;
237
209
  }
238
210
  }
239
211
  }
240
- colObj['data'] = colObj['data'].sort((o1, o2) => {
241
- if (this.ord[o1['name']] > this.ord[o2['name']])
242
- return -1;
212
+ }
243
213
 
244
- if (this.ord[o1['name']] < this.ord[o2['name']])
245
- return 1;
214
+ colObj['data'].sort((o1, o2) => this.ord[o2['name']] - this.ord[o1['name']]);
215
+ }
246
216
 
217
+ this.max = this.dataFrame!.filter.trueCount;
218
+ }
247
219
 
248
- return 0;
249
- });
250
- }
251
- this.max = df.filter.trueCount;
252
- }
220
+ renderBarToCanvas(g: CanvasRenderingContext2D, cell: DG.GridCell, x: number, y: number, w: number, h: number) {
221
+ const name = cell.tableColumn!.name;
222
+ const colNameSize = g.measureText(name).width;
223
+ const barData = this.barStats[name];
224
+ const margin = 0.2;
225
+ const innerMargin = 0.02;
226
+ const selectLineRatio = 0.1;
227
+ let sum = 0;
253
228
 
254
- renderBarToCanvas(
255
- g: CanvasRenderingContext2D,
256
- cell: DG.GridCell,
257
- x: number,
258
- y: number,
259
- w: number,
260
- h: number,
261
- ) {
262
- const margin = 0.2;
263
- const innerMargin = 0.02;
264
- const selectLineRatio = 0.1;
265
- x = x + w * margin;
266
- y = y + h * margin / 4;
267
- w = w - w * margin * 2;
268
- h = h - h * margin;
269
- g.fillStyle = 'black';
270
- g.textBaseline = 'top';
271
- g.font = `${h * margin / 2}px`;
272
-
273
- const name = cell.tableColumn!.name;
274
- const colNameSize = g.measureText(name).width;
275
- g.fillText(name,
276
- x + (w - colNameSize)/2,
277
- y + h + h * margin / 4);
278
- const barData = this.barStats[name]? this.barStats[name]: this.barStats[name];
279
- let sum = 0;
280
- barData.forEach((obj) => {
281
- sum += obj['count'];
282
- });
283
- let curSum = 0;
284
-
285
- barData.forEach((obj, index) => {
286
- const sBarHeight = h * obj['count'] / this.max;
287
- const gapSize = sBarHeight * innerMargin;
288
- g.fillStyle = cp.getColor(obj['name']);
289
- g.fillRect(
290
- x,
291
- y + h * (this.max - sum + curSum) / this.max + gapSize / 2,
292
- w,
293
- sBarHeight - gapSize);
294
- if (h * margin / 2 <= sBarHeight - gapSize && h * margin / 2 <= w) {
295
- g.fillStyle = 'rgb(0,0,0)';
296
- g.font = `${h * margin / 2}px`;
297
- const [, aar] = cp.getColorAAPivot(obj['name']);
298
- g.fillText(aar,
299
- x + w / 2 - h * margin / 8,
300
- y + h * (this.max - sum + curSum) / this.max + gapSize / 2 + (sBarHeight - gapSize)/2 - h * margin / 8);
301
- }
229
+ barData.forEach((obj) => {
230
+ sum += obj['count'];
231
+ });
302
232
 
303
- if (this.selectionMode && obj['selectedCount'] > 0) {
304
- g.fillStyle = 'rgb(255,165,0)';
305
- g.fillRect(
306
- x - w * selectLineRatio * 1.5,
307
- y + h * (this.max - sum + curSum) / this.max + gapSize / 2,
308
- w * selectLineRatio,
309
- h * obj['selectedCount'] / this.max - gapSize);
233
+ x = x + w * margin;
234
+ y = y + h * margin / 4;
235
+ w = w - w * margin * 2;
236
+ h = h - h * margin;
237
+ const barWidth = w - 10;
238
+ g.fillStyle = 'black';
239
+ g.textBaseline = 'top';
240
+ g.font = `${h * margin / 2}px`;
241
+ g.fillText(name, x + (w - colNameSize) / 2, y + h + h * margin / 4);
242
+
243
+ barData.forEach((obj) => {
244
+ const sBarHeight = h * obj['count'] / this.max;
245
+ const gapSize = sBarHeight * innerMargin;
246
+ const verticalShift = (this.max - sum) / this.max;
247
+ const [color, aarOuter] = PeptidesController.chemPalette.getColorAAPivot(obj['name']);
248
+ const textSize = g.measureText(aarOuter);
249
+ const fontSize = 11;
250
+ const leftMargin = (w - (aarOuter.length > 1 ? fontSize : textSize.width - 8)) / 2;
251
+ const subBartHeight = sBarHeight - gapSize;
252
+ const yStart = h * verticalShift + gapSize / 2;
253
+ const xStart = (w - barWidth) / 2;
254
+ const absX = x + leftMargin;
255
+ const absY = y + yStart + subBartHeight / 2 + (aarOuter.length == 1 ? + 4 : 0);
256
+ const eps = 0.1;
257
+
258
+ g.strokeStyle = color;
259
+ g.fillStyle = color;
260
+ if (textSize.width <= subBartHeight) {
261
+ const origTransform = g.getTransform();
262
+
263
+ if (color != PeptidesController.chemPalette.undefinedColor) {
264
+ g.fillRect(x + xStart, y + yStart, barWidth, subBartHeight);
265
+ g.fillStyle = 'black';
266
+ } else
267
+ g.strokeRect(x + xStart + 0.5, y + yStart, barWidth - 1, subBartHeight);
268
+
269
+ g.font = `${fontSize}px monospace`;
270
+ g.textAlign = 'center';
271
+ g.textBaseline = 'bottom';
272
+
273
+ if (aarOuter.length > 1) {
274
+ g.translate(absX, absY);
275
+ g.rotate(Math.PI / 2);
276
+ g.translate(-absX, -absY);
310
277
  }
311
278
 
312
- // @ts-ignore
313
- if (this.dataFrame.currentRow[name] === obj['name']) {
314
- g.strokeStyle = 'rgb(0,0,0)';
315
- g.strokeRect(
316
- x,
317
- y + h * (this.max - sum + curSum) / this.max + gapSize / 2,
318
- w,
319
- sBarHeight - gapSize);
320
- }
279
+ g.fillText(aarOuter, absX, absY);
280
+ g.setTransform(origTransform);
281
+ } else
282
+ g.fillRect(x + xStart, y + yStart, barWidth, subBartHeight);
321
283
 
322
- curSum += obj['count'];
323
- });
324
- return;
325
- }
326
-
327
- render(computeData = true) {
328
- const df = this.dataFrame!;
329
- if (computeData)
330
- this.computeData(df);
284
+ if (obj['selectedCount'] > eps) {
285
+ g.fillStyle = 'rgb(255,165,0)';
286
+ g.fillRect(
287
+ x + xStart - w * selectLineRatio * 2,
288
+ y + yStart,
289
+ barWidth * selectLineRatio,
290
+ h * obj['selectedCount'] / this.max - gapSize,
291
+ );
292
+ }
331
293
 
332
- if (this.tableCanvas) {
333
- for (const name of this.aminoColumnNames)
334
- this.renderBar(name);
294
+ if (obj['highlightedCount'] > eps && obj['highlightedCount'] > obj['selectedCount']) {
295
+ g.fillStyle = 'rgb(209,242,251)';
296
+ g.fillRect(
297
+ x + xStart - w * selectLineRatio * 2,
298
+ y + yStart + h * obj['selectedCount'] / this.max - gapSize,
299
+ barWidth * selectLineRatio,
300
+ h * (obj['highlightedCount'] - obj['selectedCount']) / this.max - gapSize,
301
+ );
335
302
  }
336
- return;
337
- }
338
303
 
339
- onPropertyChanged(property: DG.Property) {
340
- super.onPropertyChanged(property);
341
- this.render();
342
- }
304
+ sum -= obj['count'];
305
+ });
306
+ }
343
307
 
344
- register(args: DG.GridCellRenderArgs) {
345
- this.registered[args.cell.tableColumn!.name] = args.cell;
346
- }
308
+ highlight(cell: DG.GridCell, offsetX:number, offsetY:number, mouseEvent: MouseEvent) {
309
+ if (!cell.tableColumn?.name || !this.aminoColumnNames.includes(cell.tableColumn.name))
310
+ return;
347
311
 
348
- unregister(name: string) {
349
- if (this.registered[name])
350
- delete this.registered[name];
351
- }
312
+ const colName = cell.tableColumn?.name;
313
+ const innerMargin = 0.02;
314
+ const margin = 0.2;
315
+ const bound = cell.bounds;
316
+ const height = 130;
317
+ const x = bound.x + bound.width * margin;
318
+ const y = height * margin / 4;
319
+ const w = bound.width - bound.width * margin * 2;
320
+ const h = height - height * margin;
321
+ const barData = this.barStats[colName];
322
+ const barWidth = w - 10;
323
+ let sum = 0;
324
+
325
+ barData.forEach((obj) => {
326
+ sum += obj['count'];
327
+ });
352
328
 
329
+ this.highlighted = null;
330
+ barData.forEach((obj) => {
331
+ const sBarHeight = h * obj['count'] / this.max;
332
+ const gapSize = sBarHeight * innerMargin;
333
+ const verticalShift = (this.max - sum) / this.max;
334
+ const subBartHeight = sBarHeight - gapSize;
335
+ const yStart = h * verticalShift + gapSize / 2;
336
+ const xStart = (w - barWidth) / 2;
353
337
 
354
- renderBar(name: string) {
355
- if (!(this.registered[name]) || !(this.tableCanvas))
356
- return;
338
+ if (offsetX >= x + xStart &&
339
+ offsetY >= y + yStart &&
340
+ offsetX <= x + xStart + barWidth &&
341
+ offsetY <= y + yStart + subBartHeight)
342
+ this.highlighted = {'colName': colName, 'aaName': obj['name']};
357
343
 
358
- const cell = this.registered[name];
359
- const rect = cell.bounds;
360
- this.renderBarToCanvas(this.tableCanvas.getContext('2d')!, cell, rect.x, rect.y, rect.width, rect.height);
361
- }
362
344
 
363
- highlight(cell: DG.GridCell, offsetX:number, offsetY:number) {
364
- const colName = cell.tableColumn?.name;
365
- if (!colName)
366
- return;
367
-
368
- const margin = 0.2;
369
- const bound = cell.bounds;
370
- const x = bound.x + bound.width * margin;
371
- const y = 0 + 200 * margin / 4;
372
- const w = bound.width - bound.width * margin * 2;
373
- const h = 200 - 200 * margin / 2;
374
- const barData = this.barStats[colName];
375
- let sum = 0;
376
- barData.forEach((obj) => {
377
- sum += obj['count'];
378
- });
379
- let curSum = 0;
380
-
381
- barData.forEach((obj, index) => {
382
- const sBarHeight = h * obj['count'] / this.max;
383
- if (offsetX>=x &&
384
- offsetY>=y + h * (this.max - sum + curSum) / this.max &&
385
- offsetX<=x+w &&
386
- offsetY<=y + h * (this.max - sum + curSum) / this.max + sBarHeight) {
387
- this.highlighted = {'colName': colName, 'aaName': obj['name']};
388
- return;
389
- }
345
+ sum -= obj['count'];
346
+ });
390
347
 
391
- curSum += obj['count'];
392
- });
348
+ if (!this.highlighted)
393
349
  return;
394
- }
395
350
 
396
- unhighlight() {
397
- this.highlighted = null;
351
+ if (mouseEvent.type == 'click') {
352
+ let idx = -1;
353
+
354
+ for (let i = 0; i < this.selected.length; ++i) {
355
+ if (JSON.stringify(this.selected[i]) == JSON.stringify(this.highlighted))
356
+ idx = i;
357
+ }
358
+
359
+ if (mouseEvent.shiftKey && idx == -1)
360
+ this.selected.push(this.highlighted);
361
+
362
+ if (mouseEvent.shiftKey && (mouseEvent.ctrlKey || mouseEvent.metaKey) && idx != -1)
363
+ this.selected.splice(idx, 1);
398
364
  }
365
+ }
399
366
 
400
- beginSelection(event:any) {
401
- if (!this.highlighted || !this.dataFrame)
402
- return;
367
+ unhighlight() {
368
+ this.highlighted = null;
369
+ this.highlightedMask!.setAll(false);
370
+ this.computeData();
371
+ }
403
372
 
404
- this.dataFrame!.selection.handleClick((i) => {
405
- // @ts-ignore
406
- return this.highlighted!['aaName'] === (this.dataFrame.getCol(this.highlighted!['colName']).get(i));
373
+ beginSelection(event: MouseEvent) {
374
+ if (!this.dataFrame)
375
+ return;
376
+
377
+ this.highlightedMask!.setAll(false);
378
+
379
+ this.dataFrame.selection.handleClick((i: number) => {
380
+ for (const high of this.selected) {
381
+ if (high['aaName'] === (this.dataFrame!.getCol(high['colName']).get(i)))
382
+ return true;
383
+ }
384
+ return false;
385
+ }, event);
386
+
387
+ if (this.highlighted) {
388
+ this.dataFrame.rows.match({[this.highlighted['colName']]: this.highlighted['aaName']}).highlight();
389
+ this.highlightedMask!.handleClick((i: number) => {
390
+ if (this.highlighted!['aaName'] === (this.dataFrame!.getCol(this.highlighted!['colName']).get(i)))
391
+ return true;
392
+ return false;
407
393
  }, event);
408
394
  }
395
+
396
+ this.computeData();
397
+ }
409
398
  }