@datagrok/bio 2.1.11 → 2.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +11 -12
  2. package/css/helm.css +10 -0
  3. package/detectors.js +83 -59
  4. package/dist/package-test.js +2 -68651
  5. package/dist/package-test.js.map +1 -0
  6. package/dist/package.js +2 -66040
  7. package/dist/package.js.map +1 -0
  8. package/dockerfiles/Dockerfile +86 -0
  9. package/files/icons/composition-analysis.svg +17 -0
  10. package/files/icons/sequence-diversity-viewer.svg +4 -0
  11. package/files/icons/sequence-similarity-viewer.svg +4 -0
  12. package/files/icons/vdregions-viewer.svg +22 -0
  13. package/files/icons/weblogo-viewer.svg +7 -0
  14. package/files/tests/testUrl.csv +11 -0
  15. package/files/tests/toAtomicLevelTest.csv +4 -0
  16. package/package.json +29 -32
  17. package/src/analysis/sequence-activity-cliffs.ts +15 -13
  18. package/src/analysis/sequence-diversity-viewer.ts +3 -2
  19. package/src/analysis/sequence-search-base-viewer.ts +4 -2
  20. package/src/analysis/sequence-similarity-viewer.ts +4 -4
  21. package/src/analysis/sequence-space.ts +2 -1
  22. package/src/calculations/monomerLevelMols.ts +6 -6
  23. package/src/package-test.ts +9 -2
  24. package/src/package.ts +230 -145
  25. package/src/substructure-search/substructure-search.ts +25 -22
  26. package/src/tests/Palettes-test.ts +9 -9
  27. package/src/tests/WebLogo-positions-test.ts +131 -68
  28. package/src/tests/_first-tests.ts +9 -0
  29. package/src/tests/activity-cliffs-tests.ts +8 -7
  30. package/src/tests/activity-cliffs-utils.ts +17 -9
  31. package/src/tests/bio-tests.ts +30 -21
  32. package/src/tests/checkInputColumn-tests.ts +17 -17
  33. package/src/tests/converters-test.ts +81 -46
  34. package/src/tests/detectors-benchmark-tests.ts +17 -17
  35. package/src/tests/detectors-tests.ts +190 -178
  36. package/src/tests/fasta-export-tests.ts +2 -3
  37. package/src/tests/monomer-libraries-tests.ts +34 -0
  38. package/src/tests/pepsea-tests.ts +21 -0
  39. package/src/tests/renderers-test.ts +33 -29
  40. package/src/tests/sequence-space-test.ts +6 -4
  41. package/src/tests/similarity-diversity-tests.ts +4 -4
  42. package/src/tests/splitters-test.ts +6 -7
  43. package/src/tests/substructure-filters-tests.ts +23 -1
  44. package/src/tests/utils/sequences-generators.ts +7 -7
  45. package/src/tests/utils.ts +2 -1
  46. package/src/tests/viewers.ts +16 -0
  47. package/src/utils/cell-renderer.ts +116 -54
  48. package/src/utils/constants.ts +7 -6
  49. package/src/utils/convert.ts +17 -11
  50. package/src/utils/monomer-lib.ts +174 -0
  51. package/src/utils/multiple-sequence-alignment.ts +49 -26
  52. package/src/utils/pepsea.ts +78 -0
  53. package/src/utils/save-as-fasta.ts +9 -8
  54. package/src/utils/ui-utils.ts +15 -3
  55. package/src/viewers/vd-regions-viewer.ts +125 -83
  56. package/src/viewers/web-logo-viewer.ts +1031 -0
  57. package/src/widgets/bio-substructure-filter.ts +38 -24
  58. package/tsconfig.json +71 -72
  59. package/webpack.config.js +4 -11
  60. package/dist/vendors-node_modules_datagrok-libraries_ml_src_workers_dimensionality-reducer_js.js +0 -8988
  61. package/jest.config.js +0 -33
  62. package/src/__jest__/remote.test.ts +0 -77
  63. package/src/__jest__/test-node.ts +0 -98
  64. package/test-Bio-91c83d8913ff-bb573307.html +0 -392
@@ -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
+ }