@datagrok/bio 2.1.12 → 2.4.3
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/.eslintrc.json +1 -1
- package/README.md +11 -12
- package/css/helm.css +10 -0
- package/detectors.js +97 -69
- package/dist/package-test.js +2 -13168
- package/dist/package-test.js.map +1 -0
- package/dist/package.js +2 -10560
- package/dist/package.js.map +1 -0
- package/dockerfiles/Dockerfile +86 -0
- package/files/icons/composition-analysis.svg +17 -0
- package/files/icons/sequence-diversity-viewer.svg +4 -0
- package/files/icons/sequence-similarity-viewer.svg +4 -0
- package/files/icons/vdregions-viewer.svg +22 -0
- package/files/icons/weblogo-viewer.svg +7 -0
- package/files/tests/testUrl.csv +11 -0
- package/files/tests/toAtomicLevelTest.csv +4 -0
- package/package.json +24 -25
- package/src/analysis/sequence-activity-cliffs.ts +11 -9
- package/src/analysis/sequence-search-base-viewer.ts +2 -1
- package/src/analysis/sequence-similarity-viewer.ts +3 -3
- package/src/analysis/sequence-space.ts +2 -1
- package/src/calculations/monomerLevelMols.ts +4 -4
- package/src/package-test.ts +10 -2
- package/src/package.ts +215 -131
- package/src/substructure-search/substructure-search.ts +19 -16
- package/src/tests/Palettes-test.ts +1 -1
- package/src/tests/WebLogo-positions-test.ts +113 -57
- package/src/tests/_first-tests.ts +9 -0
- package/src/tests/activity-cliffs-tests.ts +8 -7
- package/src/tests/activity-cliffs-utils.ts +17 -9
- package/src/tests/bio-tests.ts +4 -5
- package/src/tests/checkInputColumn-tests.ts +1 -1
- package/src/tests/converters-test.ts +52 -17
- package/src/tests/detectors-benchmark-tests.ts +3 -2
- package/src/tests/detectors-tests.ts +177 -172
- package/src/tests/detectors-weak-and-likely-tests.ts +129 -0
- package/src/tests/fasta-export-tests.ts +1 -1
- package/src/tests/monomer-libraries-tests.ts +34 -0
- package/src/tests/pepsea-tests.ts +21 -0
- package/src/tests/renderers-test.ts +21 -19
- package/src/tests/sequence-space-test.ts +6 -4
- package/src/tests/similarity-diversity-tests.ts +4 -4
- package/src/tests/splitters-test.ts +4 -5
- package/src/tests/substructure-filters-tests.ts +23 -1
- package/src/tests/utils/sequences-generators.ts +1 -1
- package/src/tests/utils.ts +2 -1
- package/src/tests/viewers.ts +16 -0
- package/src/utils/cell-renderer.ts +88 -35
- package/src/utils/constants.ts +7 -6
- package/src/utils/convert.ts +8 -2
- package/src/utils/monomer-lib.ts +174 -0
- package/src/utils/multiple-sequence-alignment.ts +44 -20
- package/src/utils/pepsea.ts +78 -0
- package/src/utils/save-as-fasta.ts +2 -1
- package/src/utils/ui-utils.ts +15 -3
- package/src/viewers/vd-regions-viewer.ts +113 -72
- package/src/viewers/web-logo-viewer.ts +1031 -0
- package/src/widgets/bio-substructure-filter.ts +38 -24
- package/tsconfig.json +71 -72
- package/webpack.config.js +4 -11
- package/dist/vendors-node_modules_datagrok-libraries_ml_src_workers_dimensionality-reducer_js.js +0 -9039
|
@@ -0,0 +1,1031 @@
|
|
|
1
|
+
import * as grok from 'datagrok-api/grok';
|
|
2
|
+
import * as ui from 'datagrok-api/ui';
|
|
3
|
+
import * as DG from 'datagrok-api/dg';
|
|
4
|
+
|
|
5
|
+
import wu from 'wu';
|
|
6
|
+
import * as rxjs from 'rxjs';
|
|
7
|
+
|
|
8
|
+
import {SliderOptions} from 'datagrok-api/dg';
|
|
9
|
+
import {UnitsHandler} from '@datagrok-libraries/bio/src/utils/units-handler';
|
|
10
|
+
import {SeqPalette} from '@datagrok-libraries/bio/src/seq-palettes';
|
|
11
|
+
import {
|
|
12
|
+
getSplitter, monomerToShort, pickUpPalette, pickUpSeqCol, SplitterFunc,
|
|
13
|
+
TAGS as bioTAGS
|
|
14
|
+
} from '@datagrok-libraries/bio/src/utils/macromolecule';
|
|
15
|
+
import {
|
|
16
|
+
WebLogoPropsDefault, WebLogoProps, IWebLogoViewer,
|
|
17
|
+
PositionHeight,
|
|
18
|
+
positionSeparator,
|
|
19
|
+
} from '@datagrok-libraries/bio/src/viewers/web-logo';
|
|
20
|
+
import {errorToConsole} from '@datagrok-libraries/utils/src/to-console';
|
|
21
|
+
import {TAGS as wlTAGS} from '@datagrok-libraries/bio/src/viewers/web-logo';
|
|
22
|
+
|
|
23
|
+
declare global {
|
|
24
|
+
interface HTMLCanvasElement {
|
|
25
|
+
getCursorPosition(event: MouseEvent, r: number): DG.Point;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**@param {MouseEvent} event
|
|
30
|
+
* @param {number} r devicePixelRation
|
|
31
|
+
* @return {DG.Point} canvas related cursor position
|
|
32
|
+
*/
|
|
33
|
+
HTMLCanvasElement.prototype.getCursorPosition = function(event: MouseEvent, r: number): DG.Point {
|
|
34
|
+
const rect = this.getBoundingClientRect();
|
|
35
|
+
return new DG.Point((event.clientX - rect.left) * r, (event.clientY - rect.top) * r);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
DG.Rect.prototype.contains = function(x: number, y: number): boolean {
|
|
39
|
+
return this.left <= x && x <= this.right && this.top <= y && y <= this.bottom;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export class PositionMonomerInfo {
|
|
43
|
+
/** Sequences count with monomer in position */
|
|
44
|
+
count: number;
|
|
45
|
+
|
|
46
|
+
/** Remember screen coords rect */
|
|
47
|
+
bounds: DG.Rect;
|
|
48
|
+
|
|
49
|
+
constructor(count: number = 0, bounds: DG.Rect = new DG.Rect(0, 0, 0, 0)) {
|
|
50
|
+
this.count = count;
|
|
51
|
+
this.bounds = bounds;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class PositionInfo {
|
|
56
|
+
/** Position in sequence */
|
|
57
|
+
public readonly pos: number;
|
|
58
|
+
|
|
59
|
+
/** Position name from column tag*/
|
|
60
|
+
public readonly name: string;
|
|
61
|
+
|
|
62
|
+
freq: { [m: string]: PositionMonomerInfo };
|
|
63
|
+
rowCount: number;
|
|
64
|
+
sumForHeightCalc: number;
|
|
65
|
+
|
|
66
|
+
/** freq = {}, rowCount = 0
|
|
67
|
+
* @param {string} name Name of position ('111A', '111.1', etc)
|
|
68
|
+
* @param {number} sumForHeightCalc Sum of all monomer counts for height calculation
|
|
69
|
+
* @param {number} rowCount Count of elements in column
|
|
70
|
+
* @param {string[]} freq frequency of monomers in position
|
|
71
|
+
*/
|
|
72
|
+
constructor(pos: number, name: string,
|
|
73
|
+
freq: { [m: string]: PositionMonomerInfo } = {}, rowCount: number = 0, sumForHeightCalc: number = 0
|
|
74
|
+
) {
|
|
75
|
+
this.pos = pos;
|
|
76
|
+
this.name = name;
|
|
77
|
+
this.freq = freq;
|
|
78
|
+
this.rowCount = rowCount;
|
|
79
|
+
this.sumForHeightCalc = sumForHeightCalc;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export enum VerticalAlignments {
|
|
84
|
+
TOP = 'top',
|
|
85
|
+
MIDDLE = 'middle',
|
|
86
|
+
BOTTOM = 'bottom',
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export enum HorizontalAlignments {
|
|
90
|
+
LEFT = 'left',
|
|
91
|
+
CENTER = 'center',
|
|
92
|
+
RIGHT = 'right',
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export enum PositionMarginStates {
|
|
96
|
+
AUTO = 'auto',
|
|
97
|
+
ON = 'on',
|
|
98
|
+
OFF = 'off',
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export enum FilterSources {
|
|
102
|
+
Filtered = 'Filtered',
|
|
103
|
+
Selected = 'Selected',
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export enum PROPS_CATS {
|
|
107
|
+
STYLE = 'Style',
|
|
108
|
+
BEHAVIOR = 'Behavior',
|
|
109
|
+
LAYOUT = 'Layout',
|
|
110
|
+
DATA = 'Data',
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export enum PROPS {
|
|
114
|
+
// -- Data --
|
|
115
|
+
sequenceColumnName = 'sequenceColumnName',
|
|
116
|
+
startPositionName = 'startPositionName',
|
|
117
|
+
endPositionName = 'endPositionName',
|
|
118
|
+
skipEmptySequences = 'skipEmptySequences',
|
|
119
|
+
skipEmptyPositions = 'skipEmptyPositions',
|
|
120
|
+
shrinkEmptyTail = 'shrinkEmptyTail',
|
|
121
|
+
|
|
122
|
+
// -- Style --
|
|
123
|
+
backgroundColor = 'backgroundColor',
|
|
124
|
+
positionHeight = 'positionHeight',
|
|
125
|
+
positionWidth = 'positionWidth',
|
|
126
|
+
|
|
127
|
+
// -- Layout --
|
|
128
|
+
verticalAlignment = 'verticalAlignment',
|
|
129
|
+
horizontalAlignment = 'horizontalAlignment',
|
|
130
|
+
fixWidth = 'fixWidth',
|
|
131
|
+
fitArea = 'fitArea',
|
|
132
|
+
minHeight = 'minHeight',
|
|
133
|
+
maxHeight = 'maxHeight',
|
|
134
|
+
positionMarginState = 'positionMarginState',
|
|
135
|
+
positionMargin = 'positionMargin',
|
|
136
|
+
|
|
137
|
+
// -- Behavior --
|
|
138
|
+
filterSource = 'filterSource',
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export class WebLogoViewer extends DG.JsViewer {
|
|
142
|
+
public static residuesSet = 'nucleotides';
|
|
143
|
+
private static viewerCount: number = -1;
|
|
144
|
+
|
|
145
|
+
private readonly viewerId: number = -1;
|
|
146
|
+
private unitsHandler: UnitsHandler | null;
|
|
147
|
+
private initialized: boolean = false;
|
|
148
|
+
|
|
149
|
+
// private readonly colorScheme: ColorScheme = ColorSchemes[NucleotidesWebLogo.residuesSet];
|
|
150
|
+
protected cp: SeqPalette | null = null;
|
|
151
|
+
|
|
152
|
+
private host?: HTMLDivElement;
|
|
153
|
+
private msgHost?: HTMLElement;
|
|
154
|
+
private canvas: HTMLCanvasElement;
|
|
155
|
+
private slider: DG.RangeSlider;
|
|
156
|
+
private readonly textBaseline: CanvasTextBaseline;
|
|
157
|
+
|
|
158
|
+
private axisHeight: number = 12;
|
|
159
|
+
|
|
160
|
+
private seqCol: DG.Column<string> | null = null;
|
|
161
|
+
private splitter: SplitterFunc | null = null;
|
|
162
|
+
// private maxLength: number = 100;
|
|
163
|
+
private positions: PositionInfo[] = [];
|
|
164
|
+
|
|
165
|
+
private rowsMasked: number = 0;
|
|
166
|
+
private rowsNull: number = 0;
|
|
167
|
+
private visibleSlider: boolean = false;
|
|
168
|
+
private allowResize: boolean = true;
|
|
169
|
+
private turnOfResizeForOneSetValue: boolean = false;
|
|
170
|
+
|
|
171
|
+
// Viewer's properties (likely they should be public so that they can be set outside)
|
|
172
|
+
// -- Data --
|
|
173
|
+
public sequenceColumnName: string | null;
|
|
174
|
+
public skipEmptySequences: boolean;
|
|
175
|
+
public skipEmptyPositions: boolean;
|
|
176
|
+
|
|
177
|
+
// -- Style --
|
|
178
|
+
private _positionWidth: number;
|
|
179
|
+
public positionWidth: number;
|
|
180
|
+
public minHeight: number;
|
|
181
|
+
public backgroundColor: number = 0xFFFFFFFF;
|
|
182
|
+
public maxHeight: number;
|
|
183
|
+
public positionMarginState: string;
|
|
184
|
+
public positionMargin: number = 0;
|
|
185
|
+
public startPositionName: string | null;
|
|
186
|
+
public endPositionName: string | null;
|
|
187
|
+
public fixWidth: boolean;
|
|
188
|
+
public verticalAlignment: string | null;
|
|
189
|
+
public horizontalAlignment: string | null;
|
|
190
|
+
public fitArea: boolean;
|
|
191
|
+
public shrinkEmptyTail: boolean;
|
|
192
|
+
public positionHeight: string;
|
|
193
|
+
|
|
194
|
+
// -- Behavior --
|
|
195
|
+
public filterSource: FilterSources;
|
|
196
|
+
|
|
197
|
+
private positionNames: string[] = [];
|
|
198
|
+
private startPosition: number = -1;
|
|
199
|
+
private endPosition: number = -1;
|
|
200
|
+
|
|
201
|
+
private get filter(): DG.BitSet {
|
|
202
|
+
let res: DG.BitSet;
|
|
203
|
+
switch (this.filterSource) {
|
|
204
|
+
case FilterSources.Filtered:
|
|
205
|
+
res = this.dataFrame.filter;
|
|
206
|
+
break;
|
|
207
|
+
case FilterSources.Selected:
|
|
208
|
+
res = this.dataFrame.selection;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
return res;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** For startPosition equals to endPosition Length is 1 */
|
|
215
|
+
private get Length(): number {
|
|
216
|
+
if (this.skipEmptyPositions) {
|
|
217
|
+
return this.positions.length;
|
|
218
|
+
}
|
|
219
|
+
return this.startPosition <= this.endPosition ? this.endPosition - this.startPosition + 1 : 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Calculate new position data basic on {@link positionMarginState} and {@link positionMargin} */
|
|
223
|
+
private get positionWidthWithMargin() {
|
|
224
|
+
return this._positionWidth + this.positionMarginValue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private get positionMarginValue() {
|
|
228
|
+
if ((this.positionMarginState === 'auto') && (this.unitsHandler?.getAlphabetIsMultichar() === true)) {
|
|
229
|
+
return this.positionMargin;
|
|
230
|
+
}
|
|
231
|
+
if (this.positionMarginState === 'enable') {
|
|
232
|
+
return this.positionMargin;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Count of position rendered for calculations countOfRenderPositions */
|
|
239
|
+
private get countOfRenderPositions() {
|
|
240
|
+
if (this.host == null) {
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
const r = window.devicePixelRatio;
|
|
244
|
+
if (r > 1) {
|
|
245
|
+
return this.canvasWidthWithRatio / this.positionWidthWithMargin;
|
|
246
|
+
} else {
|
|
247
|
+
return this.canvas.width / (this.positionWidthWithMargin * r);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private get canvasWidthWithRatio() {
|
|
252
|
+
return this.canvas.width * window.devicePixelRatio;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
/** Position of start rendering */
|
|
257
|
+
private get firstVisibleIndex(): number {
|
|
258
|
+
return (this.visibleSlider) ? Math.floor(this.slider.min) : 0;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private viewSubs: rxjs.Unsubscribable[] = [];
|
|
262
|
+
|
|
263
|
+
constructor() {
|
|
264
|
+
super();
|
|
265
|
+
|
|
266
|
+
this.viewerId = WebLogoViewer.viewerCount;
|
|
267
|
+
WebLogoViewer.viewerCount += 1;
|
|
268
|
+
|
|
269
|
+
this.textBaseline = 'top';
|
|
270
|
+
this.unitsHandler = null;
|
|
271
|
+
|
|
272
|
+
// -- Data --
|
|
273
|
+
this.sequenceColumnName = this.string(PROPS.sequenceColumnName, null,
|
|
274
|
+
{category: PROPS_CATS.DATA});
|
|
275
|
+
this.startPositionName = this.string(PROPS.startPositionName, null,
|
|
276
|
+
{category: PROPS_CATS.DATA});
|
|
277
|
+
this.endPositionName = this.string(PROPS.endPositionName, null,
|
|
278
|
+
{category: PROPS_CATS.DATA});
|
|
279
|
+
this.skipEmptySequences = this.bool(PROPS.skipEmptySequences, true,
|
|
280
|
+
{category: PROPS_CATS.DATA});
|
|
281
|
+
this.skipEmptyPositions = this.bool(PROPS.skipEmptyPositions, false,
|
|
282
|
+
{category: PROPS_CATS.DATA});
|
|
283
|
+
this.shrinkEmptyTail = this.bool(PROPS.shrinkEmptyTail, true,
|
|
284
|
+
{category: PROPS_CATS.DATA});
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
// -- Style --
|
|
288
|
+
this.backgroundColor = this.int(PROPS.backgroundColor, 0xFFFFFFFF,
|
|
289
|
+
{category: PROPS_CATS.STYLE});
|
|
290
|
+
this.positionHeight = this.string(PROPS.positionHeight, PositionHeight.full,
|
|
291
|
+
{category: PROPS_CATS.STYLE, choices: Object.values(PositionHeight)});
|
|
292
|
+
this._positionWidth = this.positionWidth = this.float(PROPS.positionWidth, 16,
|
|
293
|
+
{category: PROPS_CATS.STYLE/* editor: 'slider', min: 4, max: 64, postfix: 'px' */});
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
// -- Layout --
|
|
297
|
+
this.verticalAlignment = this.string(PROPS.verticalAlignment, VerticalAlignments.MIDDLE,
|
|
298
|
+
{category: PROPS_CATS.LAYOUT, choices: Object.values(VerticalAlignments)});
|
|
299
|
+
this.horizontalAlignment = this.string(PROPS.horizontalAlignment, HorizontalAlignments.CENTER,
|
|
300
|
+
{category: PROPS_CATS.LAYOUT, choices: Object.values(HorizontalAlignments)});
|
|
301
|
+
this.fixWidth = this.bool(PROPS.fixWidth, false,
|
|
302
|
+
{category: PROPS_CATS.LAYOUT});
|
|
303
|
+
this.fitArea = this.bool(PROPS.fitArea, true,
|
|
304
|
+
{category: PROPS_CATS.LAYOUT});
|
|
305
|
+
this.minHeight = this.float(PROPS.minHeight, 50,
|
|
306
|
+
{category: PROPS_CATS.LAYOUT/*, editor: 'slider', min: 25, max: 250, postfix: 'px'*/});
|
|
307
|
+
this.maxHeight = this.float(PROPS.maxHeight, 100,
|
|
308
|
+
{category: PROPS_CATS.LAYOUT/*, editor: 'slider', min: 25, max: 500, postfix: 'px'*/});
|
|
309
|
+
this.positionMarginState = this.string(PROPS.positionMarginState, PositionMarginStates.AUTO,
|
|
310
|
+
{category: PROPS_CATS.LAYOUT, choices: Object.values(PositionMarginStates)});
|
|
311
|
+
let defaultValueForPositionMargin = 0;
|
|
312
|
+
if (this.positionMarginState === 'auto') defaultValueForPositionMargin = 4;
|
|
313
|
+
this.positionMargin = this.int(PROPS.positionMargin, defaultValueForPositionMargin,
|
|
314
|
+
{category: PROPS_CATS.LAYOUT, min: 0, max: 16});
|
|
315
|
+
|
|
316
|
+
// -- Behavior --
|
|
317
|
+
this.filterSource = this.string(PROPS.filterSource, FilterSources.Filtered,
|
|
318
|
+
{category: PROPS_CATS.BEHAVIOR, choices: Object.values(FilterSources)}) as FilterSources;
|
|
319
|
+
|
|
320
|
+
const style: SliderOptions = {style: 'barbell'};
|
|
321
|
+
this.slider = ui.rangeSlider(0, 100, 0, 20, false, style);
|
|
322
|
+
this.canvas = ui.canvas();
|
|
323
|
+
this.canvas.style.width = '100%';
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private init(): void {
|
|
327
|
+
if (this.initialized) {
|
|
328
|
+
console.error('Bio: WebLogoViewer.init() second initialization!');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
this.initialized = true;
|
|
333
|
+
this.helpUrl = '/help/visualize/viewers/web-logo.md';
|
|
334
|
+
|
|
335
|
+
this.msgHost = ui.div('No message');
|
|
336
|
+
this.msgHost.style.display = 'none';
|
|
337
|
+
|
|
338
|
+
this.canvas = ui.canvas();
|
|
339
|
+
this.canvas.style.width = '100%';
|
|
340
|
+
|
|
341
|
+
//this.slider.setShowHandles(false);
|
|
342
|
+
this.slider.root.style.position = 'absolute';
|
|
343
|
+
this.slider.root.style.zIndex = '999';
|
|
344
|
+
this.slider.root.style.display = 'none';
|
|
345
|
+
this.slider.root.style.height = '0.7em';
|
|
346
|
+
|
|
347
|
+
this.visibleSlider = false;
|
|
348
|
+
|
|
349
|
+
this.subs.push(this.slider.onValuesChanged.subscribe(this.sliderOnValuesChanged.bind(this)));
|
|
350
|
+
|
|
351
|
+
this.host = ui.div([this.msgHost, this.canvas]);
|
|
352
|
+
|
|
353
|
+
this.host.style.justifyContent = 'center';
|
|
354
|
+
this.host.style.alignItems = 'center';
|
|
355
|
+
this.host.style.position = 'relative';
|
|
356
|
+
this.host.style.setProperty('overflow', 'hidden', 'important');
|
|
357
|
+
|
|
358
|
+
this.subs.push(
|
|
359
|
+
rxjs.fromEvent<MouseEvent>(this.canvas, 'mousemove').subscribe(this.canvasOnMouseMove.bind(this)));
|
|
360
|
+
this.subs.push(
|
|
361
|
+
rxjs.fromEvent<MouseEvent>(this.canvas, 'mousedown').subscribe(this.canvasOnMouseDown.bind(this)));
|
|
362
|
+
|
|
363
|
+
this.subs.push(rxjs.fromEvent<WheelEvent>(this.canvas, 'wheel').subscribe(this.canvasOnWheel.bind(this)));
|
|
364
|
+
|
|
365
|
+
this.subs.push(ui.onSizeChanged(this.root).subscribe(this.rootOnSizeChanged.bind(this)));
|
|
366
|
+
|
|
367
|
+
this.root.append(this.host);
|
|
368
|
+
this.root.append(this.slider.root);
|
|
369
|
+
|
|
370
|
+
this._calculate(window.devicePixelRatio);
|
|
371
|
+
this.updateSlider();
|
|
372
|
+
this.render(true);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Handler of changing size WebLogo */
|
|
376
|
+
private rootOnSizeChanged(): void {
|
|
377
|
+
this._calculate(window.devicePixelRatio);
|
|
378
|
+
this.updateSlider();
|
|
379
|
+
this.render(true);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Assigns {@link seqCol} and {@link cp} based on {@link sequenceColumnName} and calls {@link render}().
|
|
383
|
+
*/
|
|
384
|
+
private updateSeqCol(): void {
|
|
385
|
+
if (this.dataFrame) {
|
|
386
|
+
this.seqCol = this.sequenceColumnName ? this.dataFrame.col(this.sequenceColumnName) : null;
|
|
387
|
+
if (this.seqCol == null) {
|
|
388
|
+
this.seqCol = pickUpSeqCol(this.dataFrame);
|
|
389
|
+
this.sequenceColumnName = this.seqCol ? this.seqCol.name : null;
|
|
390
|
+
}
|
|
391
|
+
if (this.seqCol) {
|
|
392
|
+
const units: string = this.seqCol!.getTag(DG.TAGS.UNITS);
|
|
393
|
+
const separator: string = this.seqCol!.getTag(bioTAGS.separator);
|
|
394
|
+
this.splitter = getSplitter(units, separator);
|
|
395
|
+
this.unitsHandler = new UnitsHandler(this.seqCol);
|
|
396
|
+
|
|
397
|
+
this.updatePositions();
|
|
398
|
+
this.cp = pickUpPalette(this.seqCol);
|
|
399
|
+
} else {
|
|
400
|
+
this.splitter = null;
|
|
401
|
+
this.positionNames = [];
|
|
402
|
+
this.startPosition = -1;
|
|
403
|
+
this.endPosition = -1;
|
|
404
|
+
this.cp = null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
this.render();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Updates {@link positionNames} and calculates {@link startPosition} and {@link endPosition}.
|
|
411
|
+
*/
|
|
412
|
+
private updatePositions(): void {
|
|
413
|
+
if (!this.seqCol)
|
|
414
|
+
return;
|
|
415
|
+
|
|
416
|
+
let categories: (string | null) [];
|
|
417
|
+
if (this.shrinkEmptyTail) {
|
|
418
|
+
const indices: Int32Array = this.dataFrame.filter.getSelectedIndexes();
|
|
419
|
+
categories = Array.from(new Set(
|
|
420
|
+
Array.from(Array(indices.length).keys()).map((i: number) => this.seqCol!.get(indices[i]))));
|
|
421
|
+
} else {
|
|
422
|
+
categories = this.seqCol.categories;
|
|
423
|
+
}
|
|
424
|
+
const maxLength = categories.length > 0 ? Math.max(...categories.map(
|
|
425
|
+
(s) => s !== null ? this.splitter!(s).length : 0)) : 0;
|
|
426
|
+
|
|
427
|
+
// Get position names from data column tag 'positionNames'
|
|
428
|
+
const positionNamesTxt = this.seqCol.getTag(wlTAGS.positionNames);
|
|
429
|
+
// Fallback if 'positionNames' tag is not provided
|
|
430
|
+
this.positionNames = positionNamesTxt ? positionNamesTxt.split(positionSeparator).map((n) => n.trim()) :
|
|
431
|
+
[...Array(maxLength).keys()].map((jPos) => `${jPos + 1}`);
|
|
432
|
+
|
|
433
|
+
this.startPosition = (this.startPositionName && this.positionNames &&
|
|
434
|
+
this.positionNames.includes(this.startPositionName)) ?
|
|
435
|
+
this.positionNames.indexOf(this.startPositionName) : 0;
|
|
436
|
+
this.endPosition = (this.endPositionName && this.positionNames &&
|
|
437
|
+
this.positionNames.includes(this.endPositionName)) ?
|
|
438
|
+
this.positionNames.indexOf(this.endPositionName) : (maxLength - 1);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private get widthArea() {
|
|
442
|
+
return this.Length * this.positionWidth / window.devicePixelRatio;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private get heightArea() {
|
|
446
|
+
return Math.min(this.maxHeight, Math.max(this.minHeight, this.root.clientHeight));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private get xScale() {
|
|
450
|
+
return this.widthArea > 0 ? (this.root.clientWidth - this.Length * this.positionMarginValue) / this.widthArea : 0;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private get yScale() {
|
|
454
|
+
return this.root.clientHeight / this.heightArea;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private checkIsHideSlider(): boolean {
|
|
458
|
+
let showSliderWithFitArea = true;
|
|
459
|
+
const minScale = Math.min(this.xScale, this.yScale);
|
|
460
|
+
|
|
461
|
+
if (((minScale == this.xScale) || (minScale <= 1)) && (this.fitArea)) {
|
|
462
|
+
showSliderWithFitArea = false;
|
|
463
|
+
}
|
|
464
|
+
return ((this.fixWidth || Math.ceil(this.canvas.width / this.positionWidthWithMargin) >= this.Length) || (showSliderWithFitArea));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
setSliderVisibility(visible: boolean): void {
|
|
468
|
+
if (visible) {
|
|
469
|
+
this.slider.root.style.display = 'inherit';
|
|
470
|
+
this.visibleSlider = true;
|
|
471
|
+
} else {
|
|
472
|
+
this.slider.root.style.display = 'none';
|
|
473
|
+
this.visibleSlider = false;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Updates {@link slider}, needed to set slider options and to update slider position. */
|
|
478
|
+
private updateSlider(): void {
|
|
479
|
+
if (this.checkIsHideSlider()) {
|
|
480
|
+
this.setSliderVisibility(false);
|
|
481
|
+
} else {
|
|
482
|
+
this.setSliderVisibility(true);
|
|
483
|
+
}
|
|
484
|
+
if ((this.slider != null) && (this.canvas != null)) {
|
|
485
|
+
const diffEndScrollAndSliderMin = Math.max(0,
|
|
486
|
+
Math.floor(this.slider.min + this.canvas.width / this.positionWidthWithMargin) - this.Length);
|
|
487
|
+
let newMin = Math.floor(this.slider.min - diffEndScrollAndSliderMin);
|
|
488
|
+
let newMax = Math.floor(this.slider.min - diffEndScrollAndSliderMin) + Math.floor(this.canvas.width / this.positionWidthWithMargin);
|
|
489
|
+
if (this.checkIsHideSlider()) {
|
|
490
|
+
newMin = 0;
|
|
491
|
+
newMax = Math.max(newMin, this.Length - 1);
|
|
492
|
+
}
|
|
493
|
+
this.turnOfResizeForOneSetValue = true;
|
|
494
|
+
this.slider.setValues(0, this.Length,
|
|
495
|
+
newMin, newMax);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** Handler of property change events. */
|
|
500
|
+
public override onPropertyChanged(property: DG.Property): void {
|
|
501
|
+
super.onPropertyChanged(property);
|
|
502
|
+
|
|
503
|
+
switch (property.name) {
|
|
504
|
+
case PROPS.sequenceColumnName:
|
|
505
|
+
case PROPS.startPositionName:
|
|
506
|
+
case PROPS.endPositionName:
|
|
507
|
+
case PROPS.filterSource:
|
|
508
|
+
this.updateSeqCol();
|
|
509
|
+
break;
|
|
510
|
+
case PROPS.positionWidth:
|
|
511
|
+
this._positionWidth = this.positionWidth;
|
|
512
|
+
this.updateSlider();
|
|
513
|
+
break;
|
|
514
|
+
case PROPS.fixWidth:
|
|
515
|
+
case PROPS.fitArea:
|
|
516
|
+
case PROPS.positionMargin:
|
|
517
|
+
this.updateSlider();
|
|
518
|
+
break;
|
|
519
|
+
case PROPS.shrinkEmptyTail:
|
|
520
|
+
case PROPS.skipEmptyPositions:
|
|
521
|
+
this.updatePositions();
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
this.render(true);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Add filter handlers when table is a attached */
|
|
529
|
+
public override onTableAttached() {
|
|
530
|
+
super.onTableAttached();
|
|
531
|
+
|
|
532
|
+
const dataFrameTxt: string = this.dataFrame ? 'data' : 'null';
|
|
533
|
+
console.debug(`Bio: WebLogoViewer<${this.viewerId}>.onTableAttached( dataFrame = ${dataFrameTxt} ) start`);
|
|
534
|
+
|
|
535
|
+
this.updateSeqCol();
|
|
536
|
+
|
|
537
|
+
if (this.dataFrame !== undefined) {
|
|
538
|
+
this.viewSubs.push(this.dataFrame.filter.onChanged.subscribe(this.dataFrameFilterOnChanged.bind(this)));
|
|
539
|
+
this.viewSubs.push(this.dataFrame.selection.onChanged.subscribe(this.dataFrameSelectionOnChanged.bind(this)));
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
this.init();
|
|
543
|
+
console.debug(`Bio: WebLogoViewer<${this.viewerId}>.onTableAttached() end`);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** Remove all handlers when table is a detach */
|
|
547
|
+
public override async detach() {
|
|
548
|
+
const dataFrameTxt = `${this.dataFrame ? 'data' : 'null'}`;
|
|
549
|
+
console.debug(`Bio: WebLogoViewer<${this.viewerId}>.onTableAttached( dataFrame = ${dataFrameTxt} ) start`);
|
|
550
|
+
super.detach();
|
|
551
|
+
|
|
552
|
+
this.viewSubs.forEach((sub) => sub.unsubscribe());
|
|
553
|
+
this.host!.remove();
|
|
554
|
+
this.msgHost = undefined;
|
|
555
|
+
this.host = undefined;
|
|
556
|
+
|
|
557
|
+
this.initialized = false;
|
|
558
|
+
console.debug(`Bio: WebLogoViewer<${this.viewerId}>.onTableAttached() end`);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// -- Routines --
|
|
562
|
+
|
|
563
|
+
getMonomer(p: DG.Point): [number, string | null, PositionMonomerInfo | null] {
|
|
564
|
+
const calculatedX = p.x + this.firstVisibleIndex * this.positionWidthWithMargin;
|
|
565
|
+
const jPos = Math.floor(p.x / this.positionWidthWithMargin + this.firstVisibleIndex);
|
|
566
|
+
const position = this.positions[jPos];
|
|
567
|
+
|
|
568
|
+
if (position == undefined)
|
|
569
|
+
return [jPos, null, null];
|
|
570
|
+
|
|
571
|
+
const monomer: string | undefined = Object.keys(position.freq)
|
|
572
|
+
.find((m) => position.freq[m].bounds.contains(calculatedX, p.y));
|
|
573
|
+
if (monomer === undefined)
|
|
574
|
+
return [jPos, null, null];
|
|
575
|
+
|
|
576
|
+
return [jPos, monomer, position.freq[monomer]];
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
/** Helper function for rendering */
|
|
580
|
+
protected _nullSequence(fillerResidue = 'X'): string {
|
|
581
|
+
if (!this.skipEmptySequences)
|
|
582
|
+
return new Array(this.Length).fill(fillerResidue).join('');
|
|
583
|
+
|
|
584
|
+
return '';
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** Helper function for remove empty positions */
|
|
588
|
+
// TODO: use this function in from core
|
|
589
|
+
protected removeWhere(array: Array<any>, predicate: (T: any) => boolean): Array<any> {
|
|
590
|
+
const length = array.length;
|
|
591
|
+
let updateIterator = 0;
|
|
592
|
+
for (let deleteIterator = 0; deleteIterator < length; deleteIterator++) {
|
|
593
|
+
if (!predicate(array[deleteIterator])) {
|
|
594
|
+
array[updateIterator] = array[deleteIterator];
|
|
595
|
+
updateIterator++;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
array.length = updateIterator;
|
|
599
|
+
return array;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** Function for removing empty positions */
|
|
603
|
+
protected _removeEmptyPositions() {
|
|
604
|
+
if (this.skipEmptyPositions) {
|
|
605
|
+
this.removeWhere(this.positions, (item) => item?.freq['-']?.count === item.rowCount);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
protected _calculate(r: number) {
|
|
610
|
+
if (!this.host || !this.seqCol || !this.dataFrame)
|
|
611
|
+
return;
|
|
612
|
+
this.unitsHandler = new UnitsHandler(this.seqCol);
|
|
613
|
+
|
|
614
|
+
this.calcSize();
|
|
615
|
+
|
|
616
|
+
const posCount: number = this.startPosition <= this.endPosition ? this.endPosition - this.startPosition + 1 : 0;
|
|
617
|
+
this.positions = new Array(posCount);
|
|
618
|
+
for (let jPos = 0; jPos < this.Length; jPos++) {
|
|
619
|
+
const posName: string = this.positionNames[this.startPosition + jPos];
|
|
620
|
+
this.positions[jPos] = new PositionInfo(this.startPosition + jPos, posName);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// 2022-05-05 askalkin instructed to show WebLogo based on filter (not selection)
|
|
624
|
+
const indices = this.filter.getSelectedIndexes();
|
|
625
|
+
// const indices = this.dataFrame.selection.trueCount > 0 ? this.dataFrame.selection.getSelectedIndexes() :
|
|
626
|
+
// this.dataFrame.filter.getSelectedIndexes();
|
|
627
|
+
|
|
628
|
+
this.rowsMasked = indices.length;
|
|
629
|
+
this.rowsNull = 0;
|
|
630
|
+
|
|
631
|
+
for (const i of indices) {
|
|
632
|
+
let s: string = <string>(this.seqCol.get(i));
|
|
633
|
+
|
|
634
|
+
if (!s) {
|
|
635
|
+
s = this._nullSequence();
|
|
636
|
+
++this.rowsNull;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const seqM: string[] = this.splitter!(s);
|
|
640
|
+
for (let jPos = 0; jPos < this.Length; jPos++) {
|
|
641
|
+
const pmInfo = this.positions[jPos].freq;
|
|
642
|
+
const m: string = seqM[this.startPosition + jPos] || '-';
|
|
643
|
+
if (!(m in pmInfo))
|
|
644
|
+
pmInfo[m] = new PositionMonomerInfo();
|
|
645
|
+
pmInfo[m].count++;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
//#region Polish freq counts
|
|
650
|
+
for (let jPos = 0; jPos < this.Length; jPos++) {
|
|
651
|
+
// delete this.positions[jPos].freq['-'];
|
|
652
|
+
|
|
653
|
+
this.positions[jPos].rowCount = 0;
|
|
654
|
+
for (const m in this.positions[jPos].freq)
|
|
655
|
+
this.positions[jPos].rowCount += this.positions[jPos].freq[m].count;
|
|
656
|
+
if (this.positionHeight == PositionHeight.Entropy) {
|
|
657
|
+
this.positions[jPos].sumForHeightCalc = 0;
|
|
658
|
+
for (const m in this.positions[jPos].freq) {
|
|
659
|
+
const pn = this.positions[jPos].freq[m].count / this.positions[jPos].rowCount;
|
|
660
|
+
this.positions[jPos].sumForHeightCalc += -pn * Math.log2(pn);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
//#endregion
|
|
665
|
+
this._removeEmptyPositions();
|
|
666
|
+
|
|
667
|
+
const absoluteMaxHeight = this.canvas.height - this.axisHeight * r;
|
|
668
|
+
|
|
669
|
+
//#region Calculate screen
|
|
670
|
+
for (let jPos = 0; jPos < this.Length; jPos++) {
|
|
671
|
+
const freq: { [c: string]: PositionMonomerInfo } = this.positions[jPos].freq;
|
|
672
|
+
const rowCount = this.positions[jPos].rowCount;
|
|
673
|
+
const alphabetSize = this.getAlphabetSize();
|
|
674
|
+
if ((this.positionHeight == PositionHeight.Entropy) && (alphabetSize == null)) {
|
|
675
|
+
grok.shell.error('WebLogo: alphabet is undefined.');
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const alphabetSizeLog = Math.log2(alphabetSize);
|
|
679
|
+
const maxHeight = (this.positionHeight == PositionHeight.Entropy) ?
|
|
680
|
+
(absoluteMaxHeight * (alphabetSizeLog - (this.positions[jPos].sumForHeightCalc)) / alphabetSizeLog) :
|
|
681
|
+
absoluteMaxHeight;
|
|
682
|
+
|
|
683
|
+
let y: number = this.axisHeight * r + (absoluteMaxHeight - maxHeight - 1);
|
|
684
|
+
|
|
685
|
+
const entries = Object.entries(freq).sort((a, b) => {
|
|
686
|
+
if (a[0] !== '-' && b[0] !== '-')
|
|
687
|
+
return b[1].count - a[1].count;
|
|
688
|
+
else if (a[0] === '-' && b[0] === '-')
|
|
689
|
+
return 0;
|
|
690
|
+
else if (a[0] === '-')
|
|
691
|
+
return -1;
|
|
692
|
+
else /* (b[0] === '-') */
|
|
693
|
+
return +1;
|
|
694
|
+
});
|
|
695
|
+
for (const entry of entries) {
|
|
696
|
+
const pmInfo: PositionMonomerInfo = entry[1];
|
|
697
|
+
// const m: string = entry[0];
|
|
698
|
+
const h: number = maxHeight * pmInfo.count / rowCount;
|
|
699
|
+
|
|
700
|
+
pmInfo.bounds = new DG.Rect(jPos * this.positionWidthWithMargin, y, this._positionWidth, h);
|
|
701
|
+
y += h;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
//#endregion
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/** Render WebLogo sensitive to changes in params of rendering
|
|
708
|
+
*@param {boolean} recalc - indicates that need to recalculate data for rendering
|
|
709
|
+
*/
|
|
710
|
+
render(recalc = true) {
|
|
711
|
+
if (this.msgHost) {
|
|
712
|
+
if (this.seqCol && !this.cp) {
|
|
713
|
+
this.msgHost!.innerText = `Unknown palette (column semType: '${this.seqCol.semType}').`;
|
|
714
|
+
this.msgHost!.style.display = '';
|
|
715
|
+
} else {
|
|
716
|
+
this.msgHost!.style.display = 'none';
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (!this.seqCol || !this.dataFrame || !this.cp || this.startPosition === -1 || this.endPosition === -1 || this.host == null || this.slider == null)
|
|
721
|
+
return;
|
|
722
|
+
|
|
723
|
+
const g = this.canvas.getContext('2d');
|
|
724
|
+
if (!g) return;
|
|
725
|
+
|
|
726
|
+
this.slider.root.style.width = `${this.host.clientWidth}px`;
|
|
727
|
+
|
|
728
|
+
const r = window.devicePixelRatio;
|
|
729
|
+
|
|
730
|
+
if (recalc)
|
|
731
|
+
this._calculate(r);
|
|
732
|
+
|
|
733
|
+
g.resetTransform();
|
|
734
|
+
g.fillStyle = DG.Color.toHtml(this.backgroundColor);
|
|
735
|
+
g.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
736
|
+
g.textBaseline = this.textBaseline;
|
|
737
|
+
|
|
738
|
+
const maxCountOfRowsRendered = this.countOfRenderPositions + 1;
|
|
739
|
+
const firstVisibleIndex = (this.visibleSlider) ? Math.floor(this.slider.min) : 0;
|
|
740
|
+
const lastVisibleIndex = Math.min(this.Length, firstVisibleIndex + maxCountOfRowsRendered);
|
|
741
|
+
|
|
742
|
+
//#region Plot positionNames
|
|
743
|
+
const positionFontSize = 10 * r;
|
|
744
|
+
g.resetTransform();
|
|
745
|
+
g.fillStyle = 'black';
|
|
746
|
+
g.textAlign = 'center';
|
|
747
|
+
g.font = `${positionFontSize.toFixed(1)}px Roboto, Roboto Local, sans-serif`;
|
|
748
|
+
const posNameMaxWidth = Math.max(...this.positions.map((pos) => g.measureText(pos.name).width));
|
|
749
|
+
const hScale = posNameMaxWidth < (this._positionWidth - 2) ? 1 : (this._positionWidth - 2) / posNameMaxWidth;
|
|
750
|
+
|
|
751
|
+
for (let jPos = this.firstVisibleIndex; jPos < lastVisibleIndex; jPos++) {
|
|
752
|
+
const pos: PositionInfo = this.positions[jPos];
|
|
753
|
+
g.resetTransform();
|
|
754
|
+
g.setTransform(
|
|
755
|
+
hScale, 0, 0, 1,
|
|
756
|
+
jPos * this.positionWidthWithMargin + this._positionWidth / 2 - this.positionWidthWithMargin * firstVisibleIndex, 0);
|
|
757
|
+
g.fillText(pos.name, 0, 0);
|
|
758
|
+
}
|
|
759
|
+
//#endregion Plot positionNames
|
|
760
|
+
const fontStyle = '16px Roboto, Roboto Local, sans-serif';
|
|
761
|
+
// Hacks to scale uppercase characters to target rectangle
|
|
762
|
+
const uppercaseLetterAscent = 0.25;
|
|
763
|
+
const uppercaseLetterHeight = 12.2;
|
|
764
|
+
for (let jPos = this.firstVisibleIndex; jPos < lastVisibleIndex; jPos++) {
|
|
765
|
+
for (const [monomer, pmInfo] of Object.entries(this.positions[jPos].freq)) {
|
|
766
|
+
if (monomer !== '-') {
|
|
767
|
+
const monomerTxt = monomerToShort(monomer, 5);
|
|
768
|
+
const b = pmInfo.bounds;
|
|
769
|
+
const left = b.left - this.positionWidthWithMargin * this.firstVisibleIndex;
|
|
770
|
+
|
|
771
|
+
g.resetTransform();
|
|
772
|
+
g.strokeStyle = 'lightgray';
|
|
773
|
+
g.lineWidth = 1;
|
|
774
|
+
g.rect(left, b.top, b.width, b.height);
|
|
775
|
+
g.fillStyle = this.cp.get(monomer) ?? this.cp.get('other');
|
|
776
|
+
g.textAlign = 'left';
|
|
777
|
+
g.font = fontStyle;
|
|
778
|
+
//g.fillRect(b.left, b.top, b.width, b.height);
|
|
779
|
+
const mTm: TextMetrics = g.measureText(monomerTxt);
|
|
780
|
+
|
|
781
|
+
g.setTransform(
|
|
782
|
+
b.width / mTm.width, 0, 0, b.height / uppercaseLetterHeight,
|
|
783
|
+
left, b.top);
|
|
784
|
+
g.fillText(monomerTxt, 0, -uppercaseLetterAscent);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/** Calculate canvas size an positionWidth and updates properties */
|
|
791
|
+
private calcSize() {
|
|
792
|
+
if (!this.host)
|
|
793
|
+
return;
|
|
794
|
+
|
|
795
|
+
const r: number = window.devicePixelRatio;
|
|
796
|
+
|
|
797
|
+
let width: number = this.widthArea;
|
|
798
|
+
let height = this.heightArea;
|
|
799
|
+
|
|
800
|
+
if ((this.fitArea) && (!this.visibleSlider)) {
|
|
801
|
+
const scale = Math.max(1, Math.min(this.xScale, this.yScale));
|
|
802
|
+
width = width * scale;
|
|
803
|
+
height = height * scale;
|
|
804
|
+
this._positionWidth = this.positionWidth * scale;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
width = this.Length * this.positionWidthWithMargin / r;
|
|
808
|
+
|
|
809
|
+
this.canvas.width = this.root.clientWidth * r;
|
|
810
|
+
this.canvas.style.width = `${this.root.clientWidth}px`;
|
|
811
|
+
|
|
812
|
+
// const canvasHeight: number = width > this.root.clientWidth ? height - 8 : height;
|
|
813
|
+
this.host.style.setProperty('height', `${height}px`);
|
|
814
|
+
const canvasHeight: number = this.host.clientHeight;
|
|
815
|
+
this.canvas.height = canvasHeight * r;
|
|
816
|
+
|
|
817
|
+
// Adjust host and root width
|
|
818
|
+
if (this.fixWidth) {
|
|
819
|
+
// full width for canvas host and root
|
|
820
|
+
this.root.style.width = this.host.style.width = `${width}px`;
|
|
821
|
+
this.root.style.height = `${height}px`;
|
|
822
|
+
this.root.style.overflow = 'hidden';
|
|
823
|
+
this.host.style.setProperty('overflow-y', 'hidden', 'important');
|
|
824
|
+
} else {
|
|
825
|
+
// allow scroll canvas in root
|
|
826
|
+
this.root.style.width = this.host.style.width = '100%';
|
|
827
|
+
this.host.style.overflowX = 'auto!important';
|
|
828
|
+
this.host.style.setProperty('text-align', this.horizontalAlignment);
|
|
829
|
+
|
|
830
|
+
const sliderHeight = this.visibleSlider ? 10 : 0;
|
|
831
|
+
|
|
832
|
+
// vertical alignment
|
|
833
|
+
let hostTopMargin = 0;
|
|
834
|
+
switch (this.verticalAlignment) {
|
|
835
|
+
case 'top':
|
|
836
|
+
hostTopMargin = 0;
|
|
837
|
+
break;
|
|
838
|
+
case 'middle':
|
|
839
|
+
hostTopMargin = Math.max(0, (this.root.clientHeight - height) / 2);
|
|
840
|
+
break;
|
|
841
|
+
case 'bottom':
|
|
842
|
+
hostTopMargin = Math.max(0, this.root.clientHeight - height - sliderHeight);
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
// horizontal alignment
|
|
846
|
+
let hostLeftMargin = 0;
|
|
847
|
+
switch (this.horizontalAlignment) {
|
|
848
|
+
case 'left':
|
|
849
|
+
hostLeftMargin = 0;
|
|
850
|
+
break;
|
|
851
|
+
case 'center':
|
|
852
|
+
hostLeftMargin = Math.max(0, (this.root.clientWidth - width) / 2);
|
|
853
|
+
break;
|
|
854
|
+
case 'right':
|
|
855
|
+
hostLeftMargin = Math.max(0, this.root.clientWidth - width);
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
this.host.style.setProperty('margin-top', `${hostTopMargin}px`, 'important');
|
|
859
|
+
this.host.style.setProperty('margin-left', `${hostLeftMargin}px`, 'important');
|
|
860
|
+
if (this.slider != null) {
|
|
861
|
+
this.slider.root.style.setProperty('margin-top', `${hostTopMargin + canvasHeight}px`, 'important');
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (this.root.clientHeight <= height) {
|
|
865
|
+
this.host.style.setProperty('height', `${this.root.clientHeight}px`);
|
|
866
|
+
this.host.style.setProperty('overflow-y', null);
|
|
867
|
+
} else {
|
|
868
|
+
this.host.style.setProperty('overflow-y', 'hidden', 'important');
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
public getAlphabetSize(): number {
|
|
874
|
+
return this.unitsHandler?.getAlphabetSize() ?? 0;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// -- Handle events --
|
|
878
|
+
|
|
879
|
+
private sliderOnValuesChanged(value: any): void {
|
|
880
|
+
if ((this.host == null)) return;
|
|
881
|
+
|
|
882
|
+
try {
|
|
883
|
+
/* Resize slider if we can resize do that */
|
|
884
|
+
if ((this.allowResize) && (!this.turnOfResizeForOneSetValue) &&
|
|
885
|
+
(this.visibleSlider)) {
|
|
886
|
+
const countOfPositions = Math.ceil(this.slider.max - this.slider.min);
|
|
887
|
+
const calculatedWidth = (this.canvas.width / countOfPositions) - this.positionMarginValue;
|
|
888
|
+
// saving positionWidth value global (even if slider is not visible)
|
|
889
|
+
this.positionWidth = calculatedWidth;
|
|
890
|
+
this._positionWidth = calculatedWidth;
|
|
891
|
+
}
|
|
892
|
+
this.turnOfResizeForOneSetValue = false;
|
|
893
|
+
this.render(true);
|
|
894
|
+
} catch (err: any) {
|
|
895
|
+
const errMsg = errorToConsole(err);
|
|
896
|
+
console.error('Bio: WebLogoViewer.sliderOnValuesChanged() error:\n' + errMsg);
|
|
897
|
+
//throw err; // Do not throw to prevent disabling event handler
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
private dataFrameFilterOnChanged(value: any): void {
|
|
902
|
+
console.debug('Bio: WebLogoViewer.dataFrameFilterChanged()');
|
|
903
|
+
try {
|
|
904
|
+
this.updatePositions();
|
|
905
|
+
this.render();
|
|
906
|
+
} catch (err: any) {
|
|
907
|
+
const errMsg = errorToConsole(err);
|
|
908
|
+
console.error('Bio: WebLogoViewer.dataFrameFilterOnChanged() error:\n' + errMsg);
|
|
909
|
+
//throw err; // Do not throw to prevent disabling event handler
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
private dataFrameSelectionOnChanged(value: any): void {
|
|
914
|
+
console.debug('Bio: WebLogoViewer.dataFrameSelectionOnChanged()');
|
|
915
|
+
try {
|
|
916
|
+
this.render();
|
|
917
|
+
} catch (err: any) {
|
|
918
|
+
const errMsg = errorToConsole(err);
|
|
919
|
+
console.error('Bio: WebLogoViewer.dataFrameSelectionOnChanged() error:\n' + errMsg);
|
|
920
|
+
//throw err; // Do not throw to prevent disabling event handler
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
private canvasOnMouseMove(e: MouseEvent) {
|
|
925
|
+
try {
|
|
926
|
+
const args = e as MouseEvent;
|
|
927
|
+
|
|
928
|
+
const dpr: number = window.devicePixelRatio;
|
|
929
|
+
const cursorP: DG.Point = this.canvas.getCursorPosition(args, dpr);
|
|
930
|
+
const [jPos, monomer] = this.getMonomer(cursorP);
|
|
931
|
+
// if (jPos != undefined && monomer == undefined) {
|
|
932
|
+
// const preEl = ui.element('pre');
|
|
933
|
+
// preEl.innerHTML = jPos < this.positions.length ?
|
|
934
|
+
// JSON.stringify(this.positions[jPos].freq, undefined, 2) : 'NO jPos';
|
|
935
|
+
// const tooltipEl = ui.div([ui.div(`pos: ${jPos}`), preEl]);
|
|
936
|
+
// ui.tooltip.show(tooltipEl, args.x + 16, args.y + 16);
|
|
937
|
+
// } else
|
|
938
|
+
if (this.dataFrame && this.seqCol && this.splitter && monomer) {
|
|
939
|
+
const atPI: PositionInfo = this.positions[jPos];
|
|
940
|
+
const monomerAtPosSeqCount = countForMonomerAtPosition(
|
|
941
|
+
this.dataFrame, this.seqCol, this.filter, this.splitter, monomer, atPI);
|
|
942
|
+
|
|
943
|
+
const tooltipEl = ui.div([
|
|
944
|
+
// ui.div(`pos ${jPos}`),
|
|
945
|
+
ui.div(`${monomer}`),
|
|
946
|
+
ui.div(`${monomerAtPosSeqCount} rows`)]);
|
|
947
|
+
ui.tooltip.show(tooltipEl, args.x + 16, args.y + 16);
|
|
948
|
+
} else {
|
|
949
|
+
ui.tooltip.hide();
|
|
950
|
+
}
|
|
951
|
+
} catch (err: any) {
|
|
952
|
+
const errMsg = errorToConsole(err);
|
|
953
|
+
console.error('Bio: WebLogoViewer.canvasOnMouseMove() error:\n' + errMsg);
|
|
954
|
+
//throw err; // Do not throw to prevent disabling event handler
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
private canvasOnMouseDown(e: MouseEvent): void {
|
|
959
|
+
try {
|
|
960
|
+
const args = e as MouseEvent;
|
|
961
|
+
const r: number = window.devicePixelRatio;
|
|
962
|
+
const [jPos, monomer] = this.getMonomer(this.canvas.getCursorPosition(args, r));
|
|
963
|
+
|
|
964
|
+
// prevents deselect all rows if we miss monomer bounds
|
|
965
|
+
if (this.dataFrame && this.seqCol && this.splitter && monomer) {
|
|
966
|
+
const atPI: PositionInfo = this.positions[jPos];
|
|
967
|
+
|
|
968
|
+
// this.dataFrame.selection.init((rowI: number) => {
|
|
969
|
+
// return checkSeqForMonomerAtPos(
|
|
970
|
+
// this.dataFrame, this.seqCol!, this.filter, rowI, this.splitter!, monomer, atPI);
|
|
971
|
+
// });
|
|
972
|
+
// Calculate a new BitSet object for selection to prevent interfering with existing
|
|
973
|
+
const selBS: DG.BitSet = DG.BitSet.create(this.dataFrame.selection.length, (rowI: number) => {
|
|
974
|
+
return checkSeqForMonomerAtPos(
|
|
975
|
+
this.dataFrame, this.seqCol!, this.filter, rowI, this.splitter!, monomer, atPI);
|
|
976
|
+
});
|
|
977
|
+
this.dataFrame.selection.init((i) => selBS.get(i));
|
|
978
|
+
}
|
|
979
|
+
} catch (err: any) {
|
|
980
|
+
const errMsg = errorToConsole(err);
|
|
981
|
+
console.error('Bio: WebLogoViewer.canvasOnMouseDown() error:\n' + errMsg);
|
|
982
|
+
//throw err; // Do not throw to prevent disabling event handler
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
private canvasOnWheel(e: WheelEvent) {
|
|
987
|
+
try {
|
|
988
|
+
if (!this.visibleSlider)
|
|
989
|
+
return;
|
|
990
|
+
const countOfScrollPositions = (e.deltaY / 100) * Math.max(Math.floor((this.countOfRenderPositions) / 2), 1);
|
|
991
|
+
this.slider.scrollBy(this.slider.min + countOfScrollPositions);
|
|
992
|
+
} catch (err: any) {
|
|
993
|
+
const errMsg = errorToConsole(err);
|
|
994
|
+
console.error('Bio: WebLogoViewer.canvasOnWheel() error:\n' + errMsg);
|
|
995
|
+
//throw err; // Do not throw to prevent disabling event handler
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
export function checkSeqForMonomerAtPos(
|
|
1001
|
+
df: DG.DataFrame, seqCol: DG.Column, filter: DG.BitSet, rowI: number,
|
|
1002
|
+
splitter: SplitterFunc, monomer: string, at: PositionInfo
|
|
1003
|
+
): boolean {
|
|
1004
|
+
// if (!filter.get(rowI)) return false;
|
|
1005
|
+
// TODO: Use BitSet.get(idx)
|
|
1006
|
+
if (!filter.getSelectedIndexes().includes(rowI)) return false;
|
|
1007
|
+
|
|
1008
|
+
const seq = seqCol!.get(rowI);
|
|
1009
|
+
const seqM = seq ? splitter!(seq)[at.pos] : null;
|
|
1010
|
+
return ((seqM === monomer) || (seqM === '' && monomer === '-'));
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
export function countForMonomerAtPosition(
|
|
1014
|
+
df: DG.DataFrame, seqCol: DG.Column, filter: DG.BitSet,
|
|
1015
|
+
splitter: SplitterFunc, monomer: string, at: PositionInfo
|
|
1016
|
+
): number {
|
|
1017
|
+
const posMList: (string | null)[] = wu.count(0).take(df.rowCount)
|
|
1018
|
+
.filter((rowI) => filter.get(rowI))
|
|
1019
|
+
.map((rowI) => {
|
|
1020
|
+
const seq: string | null = seqCol!.get(rowI);
|
|
1021
|
+
const seqMList: string[] = seq ? splitter!(seq) : [];
|
|
1022
|
+
const seqMPos: number = at.pos;
|
|
1023
|
+
const seqM: string | null = seqMPos < seqMList.length ? seqMList[seqMPos] : null;
|
|
1024
|
+
return seqM;
|
|
1025
|
+
}).toArray();
|
|
1026
|
+
// wu.count().take(this.dataFrame.rowCount).filter(function(iRow) {
|
|
1027
|
+
// return correctMonomerFilter(iRow, monomer, jPos);
|
|
1028
|
+
// }).reduce<number>((count, iRow) => count + 1, 0);
|
|
1029
|
+
const monomerAtPosRowCount = posMList.filter((m) => m == monomer).reduce((count, m) => count + 1, 0);
|
|
1030
|
+
return monomerAtPosRowCount;
|
|
1031
|
+
}
|