@datagrok/bio 2.4.46 → 2.4.47

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.
@@ -3,7 +3,7 @@ import * as ui from 'datagrok-api/ui';
3
3
  import * as DG from 'datagrok-api/dg';
4
4
 
5
5
  import wu from 'wu';
6
- import * as rxjs from 'rxjs';
6
+ import {fromEvent, Unsubscribable} from 'rxjs';
7
7
 
8
8
  import {SliderOptions} from 'datagrok-api/dg';
9
9
  import {UnitsHandler} from '@datagrok-libraries/bio/src/utils/units-handler';
@@ -46,12 +46,12 @@ DG.Rect.prototype.contains = function(x: number, y: number): boolean {
46
46
 
47
47
  export class PositionMonomerInfo {
48
48
  /** Sequences count with monomer in position */
49
- count: number;
49
+ public count: number;
50
50
 
51
51
  /** Remember screen coords rect */
52
- bounds: DG.Rect;
52
+ public bounds?: DG.Rect;
53
53
 
54
- constructor(count: number = 0, bounds: DG.Rect = new DG.Rect(0, 0, 0, 0)) {
54
+ constructor(count: number = 0, bounds?: DG.Rect) {
55
55
  this.count = count;
56
56
  this.bounds = bounds;
57
57
  }
@@ -64,26 +64,171 @@ export class PositionInfo {
64
64
  /** Position name from column tag*/
65
65
  public readonly name: string;
66
66
 
67
- freq: { [m: string]: PositionMonomerInfo };
67
+ private readonly _freqs: { [m: string]: PositionMonomerInfo };
68
+
68
69
  rowCount: number;
69
70
  sumForHeightCalc: number;
70
71
 
71
72
  /** freq = {}, rowCount = 0
72
73
  * @param {number} pos Position in sequence
73
74
  * @param {string} name Name of position ('111A', '111.1', etc)
74
- * @param {string[]} freq frequency of monomers in position
75
+ * @param {string[]} freqs frequency of monomers in position
75
76
  * @param {number} rowCount Count of elements in column
76
77
  * @param {number} sumForHeightCalc Sum of all monomer counts for height calculation
77
78
  */
78
79
  constructor(pos: number, name: string,
79
- freq: { [m: string]: PositionMonomerInfo } = {}, rowCount: number = 0, sumForHeightCalc: number = 0,
80
+ freqs: { [m: string]: PositionMonomerInfo } = {}, rowCount: number = 0, sumForHeightCalc: number = 0,
80
81
  ) {
81
82
  this.pos = pos;
82
83
  this.name = name;
83
- this.freq = freq;
84
+ this._freqs = freqs;
84
85
  this.rowCount = rowCount;
85
86
  this.sumForHeightCalc = sumForHeightCalc;
86
87
  }
88
+
89
+ public getMonomers(): string[] {
90
+ return Object.keys(this._freqs);
91
+ }
92
+
93
+ public getFreq(m: string): PositionMonomerInfo {
94
+ let res: PositionMonomerInfo = this._freqs[m];
95
+ if (!res) res = this._freqs[m] = new PositionMonomerInfo();
96
+ return res;
97
+ }
98
+
99
+ public setFreq(m: string, value: PositionMonomerInfo): void {
100
+
101
+ }
102
+
103
+ getMonomerAt(calculatedX: number, y: number): string | undefined {
104
+ const findRes = Object.entries(this._freqs)
105
+ .find(([m, pmInfo]) => {
106
+ return pmInfo.bounds!.contains(calculatedX, y);
107
+ });
108
+ return !!findRes ? findRes[0] : undefined;
109
+ }
110
+
111
+ calcHeights(heightMode: PositionHeight): void {
112
+ /*
113
+ this.positions[jPos].rowCount = 0;
114
+ for (const m in this.positions[jPos].freq)
115
+ this.positions[jPos].rowCount += this.positions[jPos].freq[m].count;
116
+ if (this.positionHeight == PositionHeight.Entropy) {
117
+ this.positions[jPos].sumForHeightCalc = 0;
118
+ for (const m in this.positions[jPos].freq) {
119
+ const pn = this.positions[jPos].freq[m].count / this.positions[jPos].rowCount;
120
+ this.positions[jPos].sumForHeightCalc += -pn * Math.log2(pn);
121
+ }
122
+ }
123
+ /**/
124
+
125
+ this.rowCount = 0;
126
+ for (const [m, pmInfo] of Object.entries(this._freqs))
127
+ this.rowCount += pmInfo.count;
128
+
129
+ this.sumForHeightCalc = 0;
130
+ if (heightMode === PositionHeight.Entropy) {
131
+ for (const [m, pmInfo] of Object.entries(this._freqs)) {
132
+ const pn = pmInfo.count / this.rowCount;
133
+ this.sumForHeightCalc += -pn * Math.log2(pn);
134
+ }
135
+ } else if (heightMode === PositionHeight.full) {
136
+ for (const [m, pmInfo] of Object.entries(this._freqs)) {
137
+ const pn = pmInfo.count / this.rowCount;
138
+ this.sumForHeightCalc += pn;
139
+ }
140
+ }
141
+ const k = 42;
142
+ }
143
+
144
+ calcScreen(
145
+ jPos: number, absoluteMaxHeight: number, heightMode: PositionHeight, alphabetSizeLog: number,
146
+ positionWidthWithMargin: number, positionWidth: number, r: number, axisHeight: number
147
+ ): void {
148
+ // const rowCount = this.positions[jPos].rowCount;
149
+ // const alphabetSize = this.getAlphabetSize();
150
+ // if ((this.positionHeight == PositionHeight.Entropy) && (alphabetSize == null))
151
+ // grok.shell.error('WebLogo: alphabet is undefined.');
152
+ //
153
+ // const alphabetSizeLog = Math.log2(alphabetSize);
154
+ // const maxHeight = (this.positionHeight == PositionHeight.Entropy) ?
155
+ // (absoluteMaxHeight * (alphabetSizeLog - (this.positions[jPos].sumForHeightCalc)) / alphabetSizeLog) :
156
+ // absoluteMaxHeight;
157
+ //
158
+ // let y: number = this.axisHeight * r + (absoluteMaxHeight - maxHeight - 1);
159
+ //
160
+ // const entries = Object.entries(freq).sort((a, b) => {
161
+ // if (a[0] !== '-' && b[0] !== '-')
162
+ // return b[1].count - a[1].count;
163
+ // else if (a[0] === '-' && b[0] === '-')
164
+ // return 0;
165
+ // else if (a[0] === '-')
166
+ // return -1;
167
+ // else /* (b[0] === '-') */
168
+ // return +1;
169
+ // });
170
+ // for (const entry of entries) {
171
+ // const pmInfo: PositionMonomerInfo = entry[1];
172
+ // // const m: string = entry[0];
173
+ // const h: number = maxHeight * pmInfo.count / rowCount;
174
+ //
175
+ // pmInfo.bounds = new DG.Rect(jPos * this.positionWidthWithMargin, y, this._positionWidth, h);
176
+ // y += h;
177
+ // }
178
+
179
+ const maxHeight = (heightMode == PositionHeight.Entropy) ?
180
+ (absoluteMaxHeight * (alphabetSizeLog - (this.sumForHeightCalc)) / alphabetSizeLog) :
181
+ absoluteMaxHeight;
182
+ let y: number = axisHeight * r + (absoluteMaxHeight - maxHeight - 1);
183
+
184
+ const entries = Object.entries(this._freqs)
185
+ .sort((a, b) => {
186
+ if (a[0] !== '-' && b[0] !== '-')
187
+ return b[1].count - a[1].count;
188
+ else if (a[0] === '-' && b[0] === '-')
189
+ return 0;
190
+ else if (a[0] === '-')
191
+ return -1;
192
+ else /* (b[0] === '-') */
193
+ return +1;
194
+ });
195
+ for (const entry of entries) {
196
+ const pmInfo: PositionMonomerInfo = entry[1];
197
+ // const m: string = entry[0];
198
+ const h: number = maxHeight * pmInfo.count / this.rowCount;
199
+
200
+ pmInfo.bounds = new DG.Rect(jPos * positionWidthWithMargin, y, positionWidth, h);
201
+ y += h;
202
+ }
203
+ }
204
+
205
+ render(g: CanvasRenderingContext2D,
206
+ fontStyle: string, uppercaseLetterAscent: number, uppercaseLetterHeight: number,
207
+ positionWidthWithMargin: number, firstVisibleIndex: number, cp: SeqPalette
208
+ ) {
209
+ for (const [monomer, pmInfo] of Object.entries(this._freqs)) {
210
+ if (monomer !== '-') {
211
+ const monomerTxt = monomerToShort(monomer, 5);
212
+ const b = pmInfo.bounds!;
213
+ const left = b.left - positionWidthWithMargin * firstVisibleIndex;
214
+
215
+ g.resetTransform();
216
+ g.strokeStyle = 'lightgray';
217
+ g.lineWidth = 1;
218
+ g.rect(left, b.top, b.width, b.height);
219
+ g.fillStyle = cp.get(monomer) ?? cp.get('other');
220
+ g.textAlign = 'left';
221
+ g.font = fontStyle;
222
+ //g.fillRect(b.left, b.top, b.width, b.height);
223
+ const mTm: TextMetrics = g.measureText(monomerTxt);
224
+
225
+ g.setTransform(
226
+ b.width / mTm.width, 0, 0, b.height / uppercaseLetterHeight,
227
+ left, b.top);
228
+ g.fillText(monomerTxt, 0, -uppercaseLetterAscent);
229
+ }
230
+ }
231
+ }
87
232
  }
88
233
 
89
234
  export enum PROPS_CATS {
@@ -123,6 +268,12 @@ export enum PROPS {
123
268
 
124
269
  const defaults: WebLogoProps = WebLogoPropsDefault;
125
270
 
271
+ enum RecalcLevel {
272
+ None = 0,
273
+ Layout = 1,
274
+ Freqs = 2,
275
+ }
276
+
126
277
  export class WebLogoViewer extends DG.JsViewer {
127
278
  public static residuesSet = 'nucleotides';
128
279
  private static viewerCount: number = -1;
@@ -148,7 +299,6 @@ export class WebLogoViewer extends DG.JsViewer {
148
299
  // private maxLength: number = 100;
149
300
  private positions: PositionInfo[] = [];
150
301
 
151
- private rowsMasked: number = 0;
152
302
  private rowsNull: number = 0;
153
303
  private visibleSlider: boolean = false;
154
304
  private allowResize: boolean = true;
@@ -184,6 +334,8 @@ export class WebLogoViewer extends DG.JsViewer {
184
334
  private startPosition: number = -1;
185
335
  private endPosition: number = -1;
186
336
 
337
+ private error: Error | null = null;
338
+
187
339
  private get filter(): DG.BitSet {
188
340
  let res: DG.BitSet;
189
341
  switch (this.filterSource) {
@@ -243,8 +395,6 @@ export class WebLogoViewer extends DG.JsViewer {
243
395
  return (this.visibleSlider) ? Math.floor(this.slider.min) : 0;
244
396
  }
245
397
 
246
- private viewSubs: rxjs.Unsubscribable[] = [];
247
-
248
398
  constructor() {
249
399
  super();
250
400
 
@@ -306,55 +456,6 @@ export class WebLogoViewer extends DG.JsViewer {
306
456
  this.canvas.style.width = '100%';
307
457
  }
308
458
 
309
- private init(): void {
310
- if (this.initialized) {
311
- _package.logger.error('Bio: WebLogoViewer.init() second initialization!');
312
- return;
313
- }
314
-
315
- this.initialized = true;
316
- this.helpUrl = '/help/visualize/viewers/web-logo.md';
317
-
318
- this.msgHost = ui.div('No message');
319
- this.msgHost.style.display = 'none';
320
-
321
- this.canvas = ui.canvas();
322
- this.canvas.style.width = '100%';
323
-
324
- //this.slider.setShowHandles(false);
325
- this.slider.root.style.position = 'absolute';
326
- this.slider.root.style.zIndex = '999';
327
- this.slider.root.style.display = 'none';
328
- this.slider.root.style.height = '0.7em';
329
-
330
- this.visibleSlider = false;
331
-
332
- this.subs.push(this.slider.onValuesChanged.subscribe(this.sliderOnValuesChanged.bind(this)));
333
-
334
- this.host = ui.div([this.msgHost, this.canvas]);
335
-
336
- this.host.style.justifyContent = 'center';
337
- this.host.style.alignItems = 'center';
338
- this.host.style.position = 'relative';
339
- this.host.style.setProperty('overflow', 'hidden', 'important');
340
-
341
- this.subs.push(
342
- rxjs.fromEvent<MouseEvent>(this.canvas, 'mousemove').subscribe(this.canvasOnMouseMove.bind(this)));
343
- this.subs.push(
344
- rxjs.fromEvent<MouseEvent>(this.canvas, 'mousedown').subscribe(this.canvasOnMouseDown.bind(this)));
345
-
346
- this.subs.push(rxjs.fromEvent<WheelEvent>(this.canvas, 'wheel').subscribe(this.canvasOnWheel.bind(this)));
347
-
348
- this.subs.push(ui.onSizeChanged(this.root).subscribe(this.rootOnSizeChanged.bind(this)));
349
-
350
- this.root.append(this.host);
351
- this.root.append(this.slider.root);
352
-
353
- this._calculate(window.devicePixelRatio);
354
- this.updateSlider();
355
- this.render(true);
356
- }
357
-
358
459
  // -- Data --
359
460
 
360
461
  setData(): void {
@@ -373,9 +474,14 @@ export class WebLogoViewer extends DG.JsViewer {
373
474
 
374
475
  // -- View --
375
476
 
477
+ private viewSubs: Unsubscribable[] = [];
478
+
376
479
  private destroyView() {
480
+ for (const sub of this.viewSubs) sub.unsubscribe();
481
+ this.viewSubs = [];
482
+
377
483
  const dataFrameTxt = `${this.dataFrame ? 'data' : 'null'}`;
378
- _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.onTableAttached( dataFrame = ${dataFrameTxt} ) start`);
484
+ _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.destroyView( dataFrame = ${dataFrameTxt} ) start`);
379
485
  super.detach();
380
486
 
381
487
  this.viewSubs.forEach((sub) => sub.unsubscribe());
@@ -383,30 +489,70 @@ export class WebLogoViewer extends DG.JsViewer {
383
489
  this.msgHost = undefined;
384
490
  this.host = undefined;
385
491
 
386
- this.initialized = false;
387
492
  _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.destroyView() end`);
388
493
  }
389
494
 
390
495
  private buildView() {
391
- super.onTableAttached();
392
-
393
496
  const dataFrameTxt: string = this.dataFrame ? 'data' : 'null';
394
- _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.onTableAttached( dataFrame = ${dataFrameTxt} ) start`);
497
+ _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.buildView( dataFrame = ${dataFrameTxt} ) start`);
498
+
499
+ this.helpUrl = '/help/visualize/viewers/web-logo.md';
500
+
501
+ this.msgHost = ui.div('No message');
502
+ this.msgHost.style.display = 'none';
395
503
 
396
- if (this.dataFrame !== undefined) {
504
+ this.canvas = ui.canvas();
505
+ this.canvas.style.width = '100%';
506
+
507
+ //this.slider.setShowHandles(false);
508
+ this.slider.root.style.position = 'absolute';
509
+ this.slider.root.style.zIndex = '999';
510
+ this.slider.root.style.display = 'none';
511
+ this.slider.root.style.height = '0.7em';
512
+
513
+ this.visibleSlider = false;
514
+
515
+ this.host = ui.div([this.msgHost, this.canvas]);
516
+
517
+ this.host.style.justifyContent = 'center';
518
+ this.host.style.alignItems = 'center';
519
+ this.host.style.position = 'relative';
520
+ this.host.style.setProperty('overflow', 'hidden', 'important');
521
+
522
+ this.root.append(this.host);
523
+ this.root.append(this.slider.root);
524
+
525
+ this.updateSlider();
526
+ this.render(RecalcLevel.Freqs, 'init');
527
+
528
+ if (!!this.error) {
529
+ this.msgHost!.innerText = this.error.message;
530
+ ui.tooltip.bind(this.msgHost!, this.error.stack);
531
+ this.msgHost!.style.setProperty('display', null);
532
+ }
533
+
534
+ if (this.dataFrame) {
397
535
  this.viewSubs.push(this.dataFrame.filter.onChanged.subscribe(this.dataFrameFilterOnChanged.bind(this)));
398
536
  this.viewSubs.push(this.dataFrame.selection.onChanged.subscribe(this.dataFrameSelectionOnChanged.bind(this)));
399
537
  }
400
-
401
- this.init();
402
- _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.onTableAttached() end`);
538
+ this.viewSubs.push(ui.onSizeChanged(this.root).subscribe(this.rootOnSizeChanged.bind(this)));
539
+ this.viewSubs.push(this.slider.onValuesChanged.subscribe(this.sliderOnValuesChanged.bind(this)));
540
+ this.viewSubs.push(
541
+ fromEvent<MouseEvent>(this.canvas, 'mousemove').subscribe(this.canvasOnMouseMove.bind(this)));
542
+ this.viewSubs.push(
543
+ fromEvent<MouseEvent>(this.canvas, 'mousedown').subscribe(this.canvasOnMouseDown.bind(this)));
544
+ this.viewSubs.push(
545
+ fromEvent<WheelEvent>(this.canvas, 'wheel').subscribe(this.canvasOnWheel.bind(this)));
546
+
547
+ _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.buildView() end`);
403
548
  }
404
549
 
405
550
  /** Handler of changing size WebLogo */
406
551
  private rootOnSizeChanged(): void {
407
- this._calculate(window.devicePixelRatio);
552
+ _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.rootOnSizeChanged(), start `);
553
+
408
554
  this.updateSlider();
409
- this.render(true);
555
+ this.render(RecalcLevel.Layout, 'rootOnSizeChanged');
410
556
  }
411
557
 
412
558
  /** Assigns {@link seqCol} and {@link cp} based on {@link sequenceColumnName} and calls {@link render}().
@@ -426,10 +572,11 @@ export class WebLogoViewer extends DG.JsViewer {
426
572
 
427
573
  this.updatePositions();
428
574
  this.cp = pickUpPalette(this.seqCol);
575
+ this.error = null;
429
576
  } catch (err: any) {
430
577
  this.seqCol = null;
431
- this.msgHost!.innerText = `${err}`;
432
- this.msgHost!.style.setProperty('display', null);
578
+ this.error = err instanceof Error ? err : new Error(err.toString());
579
+ _package.logger.error(this.error.message, undefined, this.error.stack);
433
580
  }
434
581
  if (!this.seqCol) {
435
582
  this.unitsHandler = null;
@@ -440,7 +587,6 @@ export class WebLogoViewer extends DG.JsViewer {
440
587
  }
441
588
  }
442
589
  }
443
- this.render();
444
590
  }
445
591
 
446
592
  /** Updates {@link positionNames} and calculates {@link startPosition} and {@link endPosition}.
@@ -449,15 +595,8 @@ export class WebLogoViewer extends DG.JsViewer {
449
595
  if (!this.seqCol)
450
596
  return;
451
597
 
452
- let categories: (string | null) [];
453
- if (this.shrinkEmptyTail) {
454
- const indices: Int32Array = this.dataFrame.filter.getSelectedIndexes();
455
- categories = Array.from(new Set(
456
- Array.from(Array(indices.length).keys()).map((i: number) => this.seqCol!.get(indices[i]))));
457
- } else {
458
- categories = this.seqCol.categories;
459
- }
460
- const maxLength = categories.length > 0 ? Math.max(...this.unitsHandler!.splitted.map((mList) => mList.length)) : 0;
598
+ const maxLength = wu(this.unitsHandler!.splitted).map((mList) => mList ? mList.length : 0)
599
+ .reduce((max, l) => Math.max(max, l), 0);
461
600
 
462
601
  // Get position names from data column tag 'positionNames'
463
602
  const positionNamesTxt = this.seqCol.getTag(wlTAGS.positionNames);
@@ -561,12 +700,12 @@ export class WebLogoViewer extends DG.JsViewer {
561
700
  break;
562
701
  }
563
702
 
564
- this.render(true);
703
+ this.render(RecalcLevel.Freqs, 'onPropertyChanged');
565
704
  }
566
705
 
567
706
  /** Add filter handlers when table is a attached */
568
707
  public override onTableAttached() {
569
- _package.logger.debug('Bio: WebLogoViewer.onTableAttached(), ');
708
+ _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.onTableAttached(), `);
570
709
 
571
710
  // -- Props editors --
572
711
 
@@ -587,17 +726,16 @@ export class WebLogoViewer extends DG.JsViewer {
587
726
  getMonomer(p: DG.Point): [number, string | null, PositionMonomerInfo | null] {
588
727
  const calculatedX = p.x + this.firstVisibleIndex * this.positionWidthWithMargin;
589
728
  const jPos = Math.floor(p.x / this.positionWidthWithMargin + this.firstVisibleIndex);
590
- const position = this.positions[jPos];
729
+ const position: PositionInfo = this.positions[jPos];
591
730
 
592
- if (position == undefined)
731
+ if (position === undefined)
593
732
  return [jPos, null, null];
594
733
 
595
- const monomer: string | undefined = Object.keys(position.freq)
596
- .find((m) => position.freq[m].bounds.contains(calculatedX, p.y));
734
+ const monomer: string | undefined = position.getMonomerAt(calculatedX, p.y);
597
735
  if (monomer === undefined)
598
736
  return [jPos, null, null];
599
737
 
600
- return [jPos, monomer, position.freq[monomer]];
738
+ return [jPos, monomer, position.getFreq(monomer)];
601
739
  };
602
740
 
603
741
  /** Helper function for rendering
@@ -632,187 +770,166 @@ export class WebLogoViewer extends DG.JsViewer {
632
770
  this.removeWhere(this.positions, (item) => item?.freq['-']?.count === item.rowCount);
633
771
  }
634
772
 
635
- protected _calculate(r: number) {
636
- if (!this.host || !this.seqCol || !this.dataFrame)
637
- return;
638
- this.unitsHandler = UnitsHandler.getOrCreate(this.seqCol);
773
+ private renderRequested: boolean = false;
774
+ /** default value of RecalcLevel.Freqs is for recalc from the scratch at the beginning */
775
+ private recalcLevelRequested: RecalcLevel = RecalcLevel.Freqs;
639
776
 
640
- this.calcSize();
777
+ /** Renders requested repeatedly will be performed once on window.requestAnimationFrame() */
778
+ render(recalcLevel: RecalcLevel, reason: string): void {
779
+ _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>` +
780
+ `.render( recalcLevel=${recalcLevel}, reason='${reason}' )`);
641
781
 
642
- const posCount: number = this.startPosition <= this.endPosition ? this.endPosition - this.startPosition + 1 : 0;
643
- this.positions = new Array(posCount);
644
- for (let jPos = 0; jPos < this.Length; jPos++) {
645
- const posName: string = this.positionNames[this.startPosition + jPos];
646
- this.positions[jPos] = new PositionInfo(this.startPosition + jPos, posName);
647
- }
782
+ /** Calculate freqs of monomers */
783
+ const calculateFreqsInt = (length: number): void => {
784
+ _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.render.calculateFreqsInt(), start `);
785
+
786
+ if (!this.host || !this.seqCol || !this.dataFrame) return;
648
787
 
649
- // 2022-05-05 askalkin instructed to show WebLogo based on filter (not selection)
650
- const selRowIndices = this.filter.getSelectedIndexes();
651
- // const indices = this.dataFrame.selection.trueCount > 0 ? this.dataFrame.selection.getSelectedIndexes() :
652
- // this.dataFrame.filter.getSelectedIndexes();
653
-
654
- this.rowsMasked = selRowIndices.length;
655
-
656
- for (const rowI of selRowIndices) {
657
- const seqM: string[] = this.unitsHandler.splitted[rowI];
658
- for (let jPos = 0; jPos < this.Length; jPos++) {
659
- const pmInfo = this.positions[jPos].freq;
660
- const m: string = seqM[this.startPosition + jPos] || '-';
661
- if (!(m in pmInfo))
662
- pmInfo[m] = new PositionMonomerInfo();
663
- pmInfo[m].count++;
788
+ this.unitsHandler = UnitsHandler.getOrCreate(this.seqCol);
789
+ const posCount: number = this.startPosition <= this.endPosition ? this.endPosition - this.startPosition + 1 : 0;
790
+ this.positions = new Array(posCount);
791
+ for (let jPos = 0; jPos < length; jPos++) {
792
+ const posName: string = this.positionNames[this.startPosition + jPos];
793
+ this.positions[jPos] = new PositionInfo(this.startPosition + jPos, posName);
664
794
  }
665
- }
666
795
 
667
- //#region Polish freq counts
668
- for (let jPos = 0; jPos < this.Length; jPos++) {
669
- // delete this.positions[jPos].freq['-'];
670
-
671
- this.positions[jPos].rowCount = 0;
672
- for (const m in this.positions[jPos].freq)
673
- this.positions[jPos].rowCount += this.positions[jPos].freq[m].count;
674
- if (this.positionHeight == PositionHeight.Entropy) {
675
- this.positions[jPos].sumForHeightCalc = 0;
676
- for (const m in this.positions[jPos].freq) {
677
- const pn = this.positions[jPos].freq[m].count / this.positions[jPos].rowCount;
678
- this.positions[jPos].sumForHeightCalc += -pn * Math.log2(pn);
796
+ // 2022-05-05 askalkin instructed to show WebLogo based on filter (not selection)
797
+ const selRowIndices = this.filter.getSelectedIndexes();
798
+
799
+ for (const rowI of selRowIndices) {
800
+ const seqMList: string[] = this.unitsHandler.splitted[rowI];
801
+ for (let jPos = 0; jPos < length; ++jPos) {
802
+ const m: string = seqMList[this.startPosition + jPos] || '-';
803
+ const pmInfo = this.positions[jPos].getFreq(m);
804
+ pmInfo.count++;
679
805
  }
680
806
  }
681
- }
682
- //#endregion
683
- this._removeEmptyPositions();
684
807
 
685
- const absoluteMaxHeight = this.canvas.height - this.axisHeight * r;
808
+ //#region Polish freq counts
809
+ for (let jPos = 0; jPos < length; jPos++) {
810
+ // delete this.positions[jPos].freq['-'];
811
+ this.positions[jPos].calcHeights(this.positionHeight as PositionHeight);
812
+ }
813
+ //#endregion
814
+ this._removeEmptyPositions();
815
+ };
816
+
817
+ /** Calculate layout of monomers on screen (canvas) based on freqs, required to handle mouse events */
818
+ const calculateLayoutInt = (length: number, dpr: number): void => {
819
+ _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.render.calculateLayoutInt(), start `);
686
820
 
687
- //#region Calculate screen
688
- for (let jPos = 0; jPos < this.Length; jPos++) {
689
- const freq: { [c: string]: PositionMonomerInfo } = this.positions[jPos].freq;
690
- const rowCount = this.positions[jPos].rowCount;
821
+ this.calcSize();
822
+ const absoluteMaxHeight = this.canvas.height - this.axisHeight * dpr;
691
823
  const alphabetSize = this.getAlphabetSize();
692
824
  if ((this.positionHeight == PositionHeight.Entropy) && (alphabetSize == null))
693
825
  grok.shell.error('WebLogo: alphabet is undefined.');
694
-
695
-
696
826
  const alphabetSizeLog = Math.log2(alphabetSize);
697
- const maxHeight = (this.positionHeight == PositionHeight.Entropy) ?
698
- (absoluteMaxHeight * (alphabetSizeLog - (this.positions[jPos].sumForHeightCalc)) / alphabetSizeLog) :
699
- absoluteMaxHeight;
700
-
701
- let y: number = this.axisHeight * r + (absoluteMaxHeight - maxHeight - 1);
702
-
703
- const entries = Object.entries(freq).sort((a, b) => {
704
- if (a[0] !== '-' && b[0] !== '-')
705
- return b[1].count - a[1].count;
706
- else if (a[0] === '-' && b[0] === '-')
707
- return 0;
708
- else if (a[0] === '-')
709
- return -1;
710
- else /* (b[0] === '-') */
711
- return +1;
712
- });
713
- for (const entry of entries) {
714
- const pmInfo: PositionMonomerInfo = entry[1];
715
- // const m: string = entry[0];
716
- const h: number = maxHeight * pmInfo.count / rowCount;
717
827
 
718
- pmInfo.bounds = new DG.Rect(jPos * this.positionWidthWithMargin, y, this._positionWidth, h);
719
- y += h;
828
+ for (let jPos = 0; jPos < length; jPos++) {
829
+ this.positions[jPos].calcScreen(jPos, absoluteMaxHeight, this.positionHeight as PositionHeight,
830
+ alphabetSizeLog, this.positionWidthWithMargin, this._positionWidth, dpr, this.axisHeight);
720
831
  }
721
- }
722
- //#endregion
723
- }
724
-
725
- /** Render WebLogo sensitive to changes in params of rendering
726
- *@param {boolean} recalc - indicates that need to recalculate data for rendering
727
- */
728
- render(recalc = true) {
729
- if (this.msgHost) {
730
- if (this.seqCol && !this.cp) {
731
- this.msgHost!.innerText = `Unknown palette (column semType: '${this.seqCol.semType}').`;
732
- this.msgHost!.style.display = '';
733
- } else {
734
- this.msgHost!.style.display = 'none';
832
+ };
833
+
834
+ /** Render WebLogo sensitive to changes in params of rendering
835
+ *@param {boolean} recalcFreqs - indicates that need to recalculate data for rendering
836
+ */
837
+ const renderInt = (recalcLevel: RecalcLevel) => {
838
+ _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.render.renderInt( recalcLevel=${recalcLevel} ), start `);
839
+ if (this.msgHost) {
840
+ if (this.seqCol && !this.cp) {
841
+ this.msgHost!.innerText = `Unknown palette (column semType: '${this.seqCol.semType}').`;
842
+ this.msgHost!.style.display = '';
843
+ } else {
844
+ this.msgHost!.style.display = 'none';
845
+ }
735
846
  }
736
- }
737
847
 
738
- if (!this.seqCol || !this.dataFrame || !this.cp || this.startPosition === -1 ||
739
- this.endPosition === -1 || this.host == null || this.slider == null)
740
- return;
848
+ if (!this.seqCol || !this.dataFrame || !this.cp || this.startPosition === -1 ||
849
+ this.endPosition === -1 || this.host == null || this.slider == null)
850
+ return;
741
851
 
742
- const g = this.canvas.getContext('2d');
743
- if (!g) return;
852
+ const g = this.canvas.getContext('2d');
853
+ if (!g) return;
744
854
 
745
- this.slider.root.style.width = `${this.host.clientWidth}px`;
855
+ this.slider.root.style.width = `${this.host.clientWidth}px`;
746
856
 
747
- const r = window.devicePixelRatio;
857
+ const length: number = this.Length;
858
+ const dpr: number = window.devicePixelRatio;
859
+ if (recalcLevel >= RecalcLevel.Freqs) calculateFreqsInt(length);
860
+ if (recalcLevel >= RecalcLevel.Layout) calculateLayoutInt(length, window.devicePixelRatio);
748
861
 
749
- if (recalc)
750
- this._calculate(r);
751
-
752
- g.resetTransform();
753
- g.fillStyle = DG.Color.toHtml(this.backgroundColor);
754
- g.fillRect(0, 0, this.canvas.width, this.canvas.height);
755
- g.textBaseline = this.textBaseline;
756
-
757
- const maxCountOfRowsRendered = this.countOfRenderPositions + 1;
758
- const firstVisibleIndex = (this.visibleSlider) ? Math.floor(this.slider.min) : 0;
759
- const lastVisibleIndex = Math.min(this.Length, firstVisibleIndex + maxCountOfRowsRendered);
760
-
761
- //#region Plot positionNames
762
- const positionFontSize = 10 * r;
763
- g.resetTransform();
764
- g.fillStyle = 'black';
765
- g.textAlign = 'center';
766
- g.font = `${positionFontSize.toFixed(1)}px Roboto, Roboto Local, sans-serif`;
767
- const posNameMaxWidth = Math.max(...this.positions.map((pos) => g.measureText(pos.name).width));
768
- const hScale = posNameMaxWidth < (this._positionWidth - 2) ? 1 : (this._positionWidth - 2) / posNameMaxWidth;
769
-
770
- for (let jPos = this.firstVisibleIndex; jPos < lastVisibleIndex; jPos++) {
771
- const pos: PositionInfo = this.positions[jPos];
772
862
  g.resetTransform();
773
- g.setTransform(
774
- hScale, 0, 0, 1,
775
- jPos * this.positionWidthWithMargin + this._positionWidth / 2 -
776
- this.positionWidthWithMargin * firstVisibleIndex, 0);
777
- g.fillText(pos.name, 0, 0);
778
- }
779
- //#endregion Plot positionNames
780
- const fontStyle = '16px Roboto, Roboto Local, sans-serif';
781
- // Hacks to scale uppercase characters to target rectangle
782
- const uppercaseLetterAscent = 0.25;
783
- const uppercaseLetterHeight = 12.2;
784
- for (let jPos = this.firstVisibleIndex; jPos < lastVisibleIndex; jPos++) {
785
- for (const [monomer, pmInfo] of Object.entries(this.positions[jPos].freq)) {
786
- if (monomer !== '-') {
787
- const monomerTxt = monomerToShort(monomer, 5);
788
- const b = pmInfo.bounds;
789
- const left = b.left - this.positionWidthWithMargin * this.firstVisibleIndex;
790
-
791
- g.resetTransform();
792
- g.strokeStyle = 'lightgray';
793
- g.lineWidth = 1;
794
- g.rect(left, b.top, b.width, b.height);
795
- g.fillStyle = this.cp.get(monomer) ?? this.cp.get('other');
796
- g.textAlign = 'left';
797
- g.font = fontStyle;
798
- //g.fillRect(b.left, b.top, b.width, b.height);
799
- const mTm: TextMetrics = g.measureText(monomerTxt);
800
-
801
- g.setTransform(
802
- b.width / mTm.width, 0, 0, b.height / uppercaseLetterHeight,
803
- left, b.top);
804
- g.fillText(monomerTxt, 0, -uppercaseLetterAscent);
805
- }
863
+ g.fillStyle = DG.Color.toHtml(this.backgroundColor);
864
+ g.fillRect(0, 0, this.canvas.width, this.canvas.height);
865
+ g.textBaseline = this.textBaseline;
866
+
867
+ const maxCountOfRowsRendered = this.countOfRenderPositions + 1;
868
+ const firstVisibleIndex = (this.visibleSlider) ? Math.floor(this.slider.min) : 0;
869
+ const lastVisibleIndex = Math.min(length, firstVisibleIndex + maxCountOfRowsRendered);
870
+
871
+ //#region Plot positionNames
872
+ const positionFontSize = 10 * dpr;
873
+ g.resetTransform();
874
+ g.fillStyle = 'black';
875
+ g.textAlign = 'center';
876
+ g.font = `${positionFontSize.toFixed(1)}px Roboto, Roboto Local, sans-serif`;
877
+ const posNameMaxWidth = Math.max(...this.positions.map((pos) => g.measureText(pos.name).width));
878
+ const hScale = posNameMaxWidth < (this._positionWidth - 2) ? 1 : (this._positionWidth - 2) / posNameMaxWidth;
879
+
880
+ for (let jPos = this.firstVisibleIndex; jPos < lastVisibleIndex; jPos++) {
881
+ const pos: PositionInfo = this.positions[jPos];
882
+ g.resetTransform();
883
+ g.setTransform(
884
+ hScale, 0, 0, 1,
885
+ jPos * this.positionWidthWithMargin + this._positionWidth / 2 -
886
+ this.positionWidthWithMargin * firstVisibleIndex, 0);
887
+ g.fillText(pos.name, 0, 0);
888
+ }
889
+ //#endregion Plot positionNames
890
+ const fontStyle = '16px Roboto, Roboto Local, sans-serif';
891
+ // Hacks to scale uppercase characters to target rectangle
892
+ const uppercaseLetterAscent = 0.25;
893
+ const uppercaseLetterHeight = 12.2;
894
+ for (let jPos = this.firstVisibleIndex; jPos < lastVisibleIndex; jPos++) {
895
+ this.positions[jPos].render(g, fontStyle, uppercaseLetterAscent, uppercaseLetterHeight,
896
+ this.positionWidthWithMargin, firstVisibleIndex, this.cp);
897
+ }
898
+
899
+ _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.render.renderInt( recalcLevel=${recalcLevel} ), end `);
900
+ };
901
+
902
+ this.recalcLevelRequested = Math.max(this.recalcLevelRequested, recalcLevel);
903
+ if (!this.renderRequested) {
904
+ this.renderRequested = true;
905
+ // requestAnimationFrame callback will be executed after this.render()
906
+ switch (this.recalcLevelRequested) {
907
+ case RecalcLevel.Freqs:
908
+ /* Avoiding [Violation] 'requestAnimationFrame' handler took too much */
909
+ window.setTimeout(() => {
910
+ renderInt(this.recalcLevelRequested);
911
+ this.recalcLevelRequested = RecalcLevel.None;
912
+ this.renderRequested = false;
913
+ }, 0 /* next event cycle */);
914
+ break;
915
+
916
+ case RecalcLevel.Layout:
917
+ case RecalcLevel.None:
918
+ window.requestAnimationFrame((time: number) => {
919
+ renderInt(this.recalcLevelRequested);
920
+ this.recalcLevelRequested = RecalcLevel.None;
921
+ this.renderRequested = false;
922
+ });
923
+ break;
806
924
  }
807
925
  }
808
926
  }
809
927
 
810
928
  /** Calculate canvas size an positionWidth and updates properties */
811
929
  private calcSize() {
812
- if (!this.host)
813
- return;
930
+ if (!this.host) return;
814
931
 
815
- const r: number = window.devicePixelRatio;
932
+ const dpr: number = window.devicePixelRatio;
816
933
 
817
934
  let width: number = this.widthArea;
818
935
  let height = this.heightArea;
@@ -824,15 +941,15 @@ export class WebLogoViewer extends DG.JsViewer {
824
941
  this._positionWidth = this.positionWidth * scale;
825
942
  }
826
943
 
827
- width = this.Length * this.positionWidthWithMargin / r;
944
+ width = this.Length * this.positionWidthWithMargin / dpr;
828
945
 
829
- this.canvas.width = this.root.clientWidth * r;
946
+ this.canvas.width = this.root.clientWidth * dpr;
830
947
  this.canvas.style.width = `${this.root.clientWidth}px`;
831
948
 
832
949
  // const canvasHeight: number = width > this.root.clientWidth ? height - 8 : height;
833
950
  this.host.style.setProperty('height', `${height}px`);
834
951
  const canvasHeight: number = this.host.clientHeight;
835
- this.canvas.height = canvasHeight * r;
952
+ this.canvas.height = canvasHeight * dpr;
836
953
 
837
954
  // Adjust host and root width
838
955
  if (this.fixWidth) {
@@ -910,33 +1027,33 @@ export class WebLogoViewer extends DG.JsViewer {
910
1027
  this._positionWidth = calculatedWidth;
911
1028
  }
912
1029
  this.turnOfResizeForOneSetValue = false;
913
- this.render(true);
1030
+ this.render(RecalcLevel.Layout, 'sliderOnValuesChanged');
914
1031
  } catch (err: any) {
915
1032
  const errMsg = errorToConsole(err);
916
- _package.logger.error('Bio: WebLogoViewer.sliderOnValuesChanged() error:\n' + errMsg);
1033
+ _package.logger.error('Bio: WebLogoViewer<${this.viewerId}>.sliderOnValuesChanged() error:\n' + errMsg);
917
1034
  //throw err; // Do not throw to prevent disabling event handler
918
1035
  }
919
1036
  }
920
1037
 
921
1038
  private dataFrameFilterOnChanged(_value: any): void {
922
- _package.logger.debug('Bio: WebLogoViewer.dataFrameFilterChanged()');
1039
+ _package.logger.debug('Bio: WebLogoViewer<${this.viewerId}>.dataFrameFilterChanged()');
923
1040
  try {
924
1041
  this.updatePositions();
925
- this.render();
1042
+ this.render(RecalcLevel.Freqs, 'dataFrameFilterOnChanged');
926
1043
  } catch (err: any) {
927
1044
  const errMsg = errorToConsole(err);
928
- _package.logger.error('Bio: WebLogoViewer.dataFrameFilterOnChanged() error:\n' + errMsg);
1045
+ _package.logger.error('Bio: WebLogoViewer<${this.viewerId}>.dataFrameFilterOnChanged() error:\n' + errMsg);
929
1046
  //throw err; // Do not throw to prevent disabling event handler
930
1047
  }
931
1048
  }
932
1049
 
933
1050
  private dataFrameSelectionOnChanged(_value: any): void {
934
- _package.logger.debug('Bio: WebLogoViewer.dataFrameSelectionOnChanged()');
1051
+ _package.logger.debug('Bio: WebLogoViewer<${this.viewerId}>.dataFrameSelectionOnChanged()');
935
1052
  try {
936
- this.render();
1053
+ this.render(RecalcLevel.Freqs, 'dataFrameSelectionOnChanged');
937
1054
  } catch (err: any) {
938
1055
  const errMsg = errorToConsole(err);
939
- _package.logger.error('Bio: WebLogoViewer.dataFrameSelectionOnChanged() error:\n' + errMsg);
1056
+ _package.logger.error('Bio: WebLogoViewer<${this.viewerId}>.dataFrameSelectionOnChanged() error:\n' + errMsg);
940
1057
  //throw err; // Do not throw to prevent disabling event handler
941
1058
  }
942
1059
  }
@@ -970,7 +1087,7 @@ export class WebLogoViewer extends DG.JsViewer {
970
1087
  }
971
1088
  } catch (err: any) {
972
1089
  const errMsg = errorToConsole(err);
973
- _package.logger.error('Bio: WebLogoViewer.canvasOnMouseMove() error:\n' + errMsg);
1090
+ _package.logger.error('Bio: WebLogoViewer<${this.viewerId}>.canvasOnMouseMove() error:\n' + errMsg);
974
1091
  //throw err; // Do not throw to prevent disabling event handler
975
1092
  }
976
1093
  }
@@ -985,10 +1102,6 @@ export class WebLogoViewer extends DG.JsViewer {
985
1102
  if (this.dataFrame && this.seqCol && this.unitsHandler && monomer) {
986
1103
  const atPI: PositionInfo = this.positions[jPos];
987
1104
 
988
- // this.dataFrame.selection.init((rowI: number) => {
989
- // return checkSeqForMonomerAtPos(
990
- // this.dataFrame, this.seqCol!, this.filter, rowI, this.splitter!, monomer, atPI);
991
- // });
992
1105
  // Calculate a new BitSet object for selection to prevent interfering with existing
993
1106
  const selBS: DG.BitSet = DG.BitSet.create(this.dataFrame.selection.length, (rowI: number) => {
994
1107
  return checkSeqForMonomerAtPos(this.dataFrame, this.unitsHandler!, this.filter, rowI, monomer, atPI);
@@ -997,7 +1110,7 @@ export class WebLogoViewer extends DG.JsViewer {
997
1110
  }
998
1111
  } catch (err: any) {
999
1112
  const errMsg = errorToConsole(err);
1000
- _package.logger.error('Bio: WebLogoViewer.canvasOnMouseDown() error:\n' + errMsg);
1113
+ _package.logger.error('Bio: WebLogoViewer<${this.viewerId}>.canvasOnMouseDown() error:\n' + errMsg);
1001
1114
  //throw err; // Do not throw to prevent disabling event handler
1002
1115
  }
1003
1116
  }
@@ -1010,7 +1123,7 @@ export class WebLogoViewer extends DG.JsViewer {
1010
1123
  this.slider.scrollBy(this.slider.min + countOfScrollPositions);
1011
1124
  } catch (err: any) {
1012
1125
  const errMsg = errorToConsole(err);
1013
- _package.logger.error('Bio: WebLogoViewer.canvasOnWheel() error:\n' + errMsg);
1126
+ _package.logger.error('Bio: WebLogoViewer<${this.viewerId}>.canvasOnWheel() error:\n' + errMsg);
1014
1127
  //throw err; // Do not throw to prevent disabling event handler
1015
1128
  }
1016
1129
  }
@@ -1031,17 +1144,13 @@ export function checkSeqForMonomerAtPos(
1031
1144
  export function countForMonomerAtPosition(
1032
1145
  df: DG.DataFrame, uh: UnitsHandler, filter: DG.BitSet, monomer: string, at: PositionInfo
1033
1146
  ): number {
1034
- const posMList: (string | null)[] = wu.count(0).take(df.rowCount)
1035
- .filter((rowI) => filter.get(rowI))
1036
- .map((rowI) => {
1037
- const seqMList: string[] = uh.splitted[rowI];
1038
- const seqMPos: number = at.pos;
1039
- const seqM: string | null = seqMPos < seqMList.length ? seqMList[seqMPos] : null;
1040
- return seqM;
1041
- }).toArray();
1042
- // wu.count().take(this.dataFrame.rowCount).filter(function(iRow) {
1043
- // return correctMonomerFilter(iRow, monomer, jPos);
1044
- // }).reduce<number>((count, iRow) => count + 1, 0);
1045
- const monomerAtPosRowCount = posMList.filter((m) => m == monomer).reduce((count, _m) => count + 1, 0);
1046
- return monomerAtPosRowCount;
1147
+ let count = 0;
1148
+ let rowI = -1;
1149
+ while ((rowI = filter.findNext(rowI, true)) != -1) {
1150
+ const seqMList: string[] = uh.splitted[rowI];
1151
+ const seqMPos: number = at.pos;
1152
+ const seqM: string | null = seqMPos < seqMList.length ? seqMList[seqMPos] : null;
1153
+ if (seqM === monomer) count++;
1154
+ }
1155
+ return count;
1047
1156
  }