@datagrok/bio 2.8.4 → 2.8.6

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.
@@ -8,25 +8,15 @@ import {fromEvent, Observable, Subject, Unsubscribable} from 'rxjs';
8
8
  import {UnitsHandler} from '@datagrok-libraries/bio/src/utils/units-handler';
9
9
  import {SeqPalette} from '@datagrok-libraries/bio/src/seq-palettes';
10
10
  import {
11
- monomerToShort,
12
- pickUpPalette,
13
- pickUpSeqCol,
14
- TAGS as bioTAGS,
11
+ monomerToShort, pickUpPalette, pickUpSeqCol, TAGS as bioTAGS
15
12
  } from '@datagrok-libraries/bio/src/utils/macromolecule';
16
13
  import {
17
- FilterSources,
18
- HorizontalAlignments,
19
- IWebLogoViewer,
20
- PositionHeight,
21
- PositionMarginStates,
22
- positionSeparator,
23
- TAGS as wlTAGS,
24
- VerticalAlignments,
25
- WebLogoProps,
26
- WebLogoPropsDefault,
14
+ FilterSources, HorizontalAlignments, IWebLogoViewer, PositionHeight, PositionMarginStates, positionSeparator,
15
+ TAGS as wlTAGS, VerticalAlignments, WebLogoProps, WebLogoPropsDefault
27
16
  } from '@datagrok-libraries/bio/src/viewers/web-logo';
28
17
  import {errorToConsole} from '@datagrok-libraries/utils/src/to-console';
29
18
  import {intToHtmlA} from '@datagrok-libraries/utils/src/color';
19
+ import {ISeqSplitted} from '@datagrok-libraries/bio/src/utils/macromolecule/types';
30
20
 
31
21
  import {_package} from '../package';
32
22
 
@@ -153,15 +143,14 @@ export class PositionInfo {
153
143
  }
154
144
 
155
145
  calcScreen(
156
- jPos: number, absoluteMaxHeight: number, heightMode: PositionHeight, alphabetSizeLog: number,
157
- positionWidthWithMargin: number, positionWidth: number, r: number, axisHeight: number
146
+ posIdx: number, firstVisiblePosIdx: number,
147
+ absoluteMaxHeight: number, heightMode: PositionHeight, alphabetSizeLog: number,
148
+ positionWidthWithMargin: number, positionWidth: number, dpr: number, axisHeight: number
158
149
  ): void {
159
- const dpr = window.devicePixelRatio;
160
-
161
150
  const maxHeight = (heightMode == PositionHeight.Entropy) ?
162
151
  (absoluteMaxHeight * (alphabetSizeLog - (this.sumForHeightCalc)) / alphabetSizeLog) :
163
152
  absoluteMaxHeight;
164
- let y: number = axisHeight * r + (absoluteMaxHeight - maxHeight - 1);
153
+ let y: number = axisHeight * dpr + (absoluteMaxHeight - maxHeight - 1);
165
154
 
166
155
  const entries = Object.entries(this._freqs)
167
156
  .sort((a, b) => {
@@ -179,20 +168,21 @@ export class PositionInfo {
179
168
  // const m: string = entry[0];
180
169
  const h: number = maxHeight * pmInfo.count / this.rowCount;
181
170
 
182
- pmInfo.bounds = new DG.Rect(jPos * dpr * positionWidthWithMargin, y, positionWidth * dpr, h);
171
+ pmInfo.bounds = new DG.Rect(
172
+ (posIdx - firstVisiblePosIdx) * dpr * positionWidthWithMargin, y,
173
+ positionWidth * dpr, h);
183
174
  y += h;
184
175
  }
185
176
  }
186
177
 
187
178
  render(g: CanvasRenderingContext2D,
188
- fontStyle: string, uppercaseLetterAscent: number, uppercaseLetterHeight: number,
189
- positionWidthWithMargin: number, firstVisibleIndex: number, cp: SeqPalette
179
+ fontStyle: string, uppercaseLetterAscent: number, uppercaseLetterHeight: number, cp: SeqPalette
190
180
  ) {
191
181
  for (const [monomer, pmInfo] of Object.entries(this._freqs)) {
192
182
  if (monomer !== '-') {
193
183
  const monomerTxt = monomerToShort(monomer, 5);
194
184
  const b = pmInfo.bounds!;
195
- const left = b.left - positionWidthWithMargin * firstVisibleIndex;
185
+ const left = b.left;
196
186
 
197
187
  g.resetTransform();
198
188
  g.strokeStyle = 'lightgray';
@@ -251,8 +241,9 @@ export enum PROPS {
251
241
 
252
242
  const defaults: WebLogoProps = WebLogoPropsDefault;
253
243
 
254
- enum RecalcLevel {
244
+ enum WlRenderLevel {
255
245
  None = 0,
246
+ Render = 1,
256
247
  Layout = 1,
257
248
  Freqs = 2,
258
249
  }
@@ -261,7 +252,7 @@ const POSITION_LABELS_HEIGHT: number = 12;
261
252
 
262
253
  export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
263
254
  public static residuesSet = 'nucleotides';
264
- private static viewerCount: number = -1;
255
+ private static viewerCount: number = 0;
265
256
 
266
257
  private viewed: boolean = false;
267
258
 
@@ -275,33 +266,35 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
275
266
  private host?: HTMLDivElement;
276
267
  private msgHost?: HTMLElement;
277
268
  private canvas: HTMLCanvasElement;
278
- private slider: DG.RangeSlider;
269
+ private readonly slider: DG.RangeSlider;
279
270
  private readonly textBaseline: CanvasTextBaseline;
280
271
 
281
272
  private seqCol: DG.Column<string> | null = null;
282
- // private maxLength: number = 100;
283
273
  private positions: PositionInfo[] = [];
284
274
 
285
- private rowsNull: number = 0;
286
275
  private visibleSlider: boolean = false;
287
276
  private allowResize: boolean = true;
288
277
  private turnOfResizeForOneSetValue: boolean = false;
289
278
 
290
- // Viewer's properties (likely they should be public so that they can be set outside)
279
+ // Viewer's properties (likely they should be public)
291
280
  // -- Data --
292
281
  public sequenceColumnName: string | null;
293
282
  public skipEmptySequences: boolean;
294
283
  public skipEmptyPositions: boolean;
295
284
 
296
285
  // -- Style --
297
- private _positionWidth: number;
298
- public positionWidth: number;
286
+ /** Gets value from properties or {@link setOptions} */ public positionWidth: number;
287
+ /** Scaled value to fit area */ private _positionWidth: number;
288
+ private _positionWidthWithMargin: number;
289
+ public get positionWidthWithMargin(): number { return this._positionWidthWithMargin; }
290
+
299
291
  public minHeight: number;
300
292
  public backgroundColor: number = 0xFFFFFFFF;
301
293
  public maxHeight: number;
302
294
  public showPositionLabels: boolean;
303
295
  public positionMarginState: PositionMarginStates;
304
- public positionMargin: number = 0;
296
+ /** Gets value from properties or setOptions */ public positionMargin: number = 0;
297
+ /** Scaled value to fit area */ private _positionMargin: number;
305
298
  public startPositionName: string | null;
306
299
  public endPositionName: string | null;
307
300
  public fixWidth: boolean;
@@ -309,7 +302,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
309
302
  public horizontalAlignment: HorizontalAlignments;
310
303
  public fitArea: boolean;
311
304
  public shrinkEmptyTail: boolean;
312
- public positionHeight: string;
305
+ public positionHeight: PositionHeight;
313
306
 
314
307
  // -- Behavior --
315
308
  public filterSource: FilterSources;
@@ -334,7 +327,9 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
334
327
  return res;
335
328
  }
336
329
 
337
- /** For startPosition equals to endPosition Length is 1 */
330
+ /** Full length of {@link positions}.
331
+ * Inclusive, for startPosition equals to endPosition Length is 1
332
+ */
338
333
  public get Length(): number {
339
334
  if (this.skipEmptyPositions)
340
335
  return this.positions.length;
@@ -342,42 +337,13 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
342
337
  return this.startPosition <= this.endPosition ? this.endPosition - this.startPosition + 1 : 0;
343
338
  }
344
339
 
345
- /** Calculate new position data basic on {@link positionMarginState} and {@link positionMargin} */
346
- private get positionWidthWithMargin() {
347
- return this._positionWidth + this.positionMarginValue;
348
- }
349
-
350
- private get positionMarginValue() {
351
- if (this.positionMarginState === PositionMarginStates.AUTO &&
352
- this.unitsHandler?.getAlphabetIsMultichar() === true
353
- ) return this.positionMargin;
354
-
355
- if (this.positionMarginState === PositionMarginStates.ON)
340
+ private get positionMarginValue(): number {
341
+ if (this.positionMarginState === PositionMarginStates.AUTO && this.unitsHandler!.getAlphabetIsMultichar() === true)
342
+ return this.positionMargin;
343
+ else if (this.positionMarginState === PositionMarginStates.ON)
356
344
  return this.positionMargin;
357
-
358
- return 0;
359
- }
360
-
361
- /** Count of position rendered for calculations countOfRenderPositions */
362
- private get countOfRenderPositions() {
363
- if (this.host == null)
364
- return 0;
365
-
366
- const r = window.devicePixelRatio;
367
- if (r > 1)
368
- return this.canvasWidthWithRatio / this.positionWidthWithMargin;
369
345
  else
370
- return this.canvas.width / (this.positionWidthWithMargin * r);
371
- }
372
-
373
- private get canvasWidthWithRatio() {
374
- return this.canvas.width * window.devicePixelRatio;
375
- }
376
-
377
-
378
- /** Position of start rendering */
379
- private get firstVisibleIndex(): number {
380
- return (this.visibleSlider) ? Math.floor(this.slider.min) : 0;
346
+ return 0;
381
347
  }
382
348
 
383
349
  constructor() {
@@ -407,7 +373,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
407
373
  this.backgroundColor = this.int(PROPS.backgroundColor, defaults.backgroundColor,
408
374
  {category: PROPS_CATS.STYLE});
409
375
  this.positionHeight = this.string(PROPS.positionHeight, defaults.positionHeight,
410
- {category: PROPS_CATS.STYLE, choices: Object.values(PositionHeight)});
376
+ {category: PROPS_CATS.STYLE, choices: Object.values(PositionHeight)}) as PositionHeight;
411
377
  this._positionWidth = this.positionWidth = this.float(PROPS.positionWidth, defaults.positionWidth,
412
378
  {category: PROPS_CATS.STYLE/* editor: 'slider', min: 4, max: 64, postfix: 'px' */});
413
379
 
@@ -417,7 +383,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
417
383
  this.horizontalAlignment = this.string(PROPS.horizontalAlignment, defaults.horizontalAlignment,
418
384
  {category: PROPS_CATS.LAYOUT, choices: Object.values(HorizontalAlignments)}) as HorizontalAlignments;
419
385
  this.fixWidth = this.bool(PROPS.fixWidth, defaults.fixWidth,
420
- {category: PROPS_CATS.LAYOUT});
386
+ {category: PROPS_CATS.LAYOUT, userEditable: false});
421
387
  this.fitArea = this.bool(PROPS.fitArea, defaults.fitArea,
422
388
  {category: PROPS_CATS.LAYOUT});
423
389
  this.minHeight = this.float(PROPS.minHeight, defaults.minHeight,
@@ -440,7 +406,10 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
440
406
  const style: DG.SliderOptions = {style: 'barbell'};
441
407
  this.slider = ui.rangeSlider(0, 100, 0, 20, false, style);
442
408
  this.canvas = ui.canvas();
409
+ this.canvas.classList.value = 'bio-wl-canvas';
443
410
  this.canvas.style.width = '100%';
411
+
412
+ /* this.root.style.background = '#FFEEDD'; */
444
413
  }
445
414
 
446
415
  // -- Data --
@@ -463,7 +432,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
463
432
 
464
433
  private viewSubs: Unsubscribable[] = [];
465
434
 
466
- private destroyView() {
435
+ private async destroyView(): Promise<void> {
467
436
  for (const sub of this.viewSubs) sub.unsubscribe();
468
437
  this.viewSubs = [];
469
438
 
@@ -479,13 +448,14 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
479
448
  _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.destroyView() end`);
480
449
  }
481
450
 
482
- private buildView() {
451
+ private async buildView(): Promise<void> {
483
452
  const dataFrameTxt: string = this.dataFrame ? 'data' : 'null';
484
453
  _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.buildView( dataFrame = ${dataFrameTxt} ) start`);
454
+ const dpr = window.devicePixelRatio;
485
455
 
486
456
  this.helpUrl = '/help/visualize/viewers/web-logo.md';
487
457
 
488
- this.msgHost = ui.div('No message');
458
+ this.msgHost = ui.div('No message', {classes: 'bio-wl-msg'});
489
459
  this.msgHost.style.display = 'none';
490
460
 
491
461
  this.canvas = ui.canvas();
@@ -499,19 +469,20 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
499
469
 
500
470
  this.visibleSlider = false;
501
471
 
502
- this.host = ui.div([this.msgHost, this.canvas]);
503
-
504
- this.host.style.justifyContent = 'center';
505
- this.host.style.alignItems = 'center';
506
- this.host.style.position = 'relative';
507
- this.host.style.setProperty('overflow', 'hidden', 'important');
472
+ this.host = ui.div([this.msgHost, this.canvas],
473
+ {
474
+ classes: 'bio-wl-host',
475
+ style: {
476
+ display: 'flex',
477
+ flexDirection: 'row',
478
+ /** For alignContent to have an effect */ flexWrap: 'wrap',
479
+ /* backgroundColor: '#EEFFEE' */
480
+ }
481
+ });
508
482
 
509
483
  this.root.append(this.host);
510
484
  this.root.append(this.slider.root);
511
485
 
512
- this.updateSlider();
513
- this.render(RecalcLevel.Freqs, 'init');
514
-
515
486
  if (!!this.error) {
516
487
  this.msgHost!.innerText = this.error.message;
517
488
  ui.tooltip.bind(this.msgHost!, this.error.stack);
@@ -531,6 +502,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
531
502
  this.viewSubs.push(
532
503
  fromEvent<WheelEvent>(this.canvas, 'wheel').subscribe(this.canvasOnWheel.bind(this)));
533
504
 
505
+ await this.render(WlRenderLevel.Freqs, 'buildView');
534
506
  _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.buildView() end`);
535
507
  }
536
508
 
@@ -538,8 +510,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
538
510
  private rootOnSizeChanged(): void {
539
511
  _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.rootOnSizeChanged(), start `);
540
512
 
541
- this.updateSlider();
542
- this.render(RecalcLevel.Layout, 'rootOnSizeChanged');
513
+ this.render(WlRenderLevel.Layout, 'rootOnSizeChanged').then(() => {});
543
514
  }
544
515
 
545
516
  /** Assigns {@link seqCol} and {@link cp} based on {@link sequenceColumnName} and calls {@link render}().
@@ -557,8 +528,8 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
557
528
  const separator: string = this.seqCol!.getTag(bioTAGS.separator);
558
529
  this.unitsHandler = UnitsHandler.getOrCreate(this.seqCol);
559
530
 
560
- this.updatePositions();
561
531
  this.cp = pickUpPalette(this.seqCol);
532
+ this.updatePositions();
562
533
  this.error = null;
563
534
  } catch (err: any) {
564
535
  this.seqCol = null;
@@ -580,8 +551,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
580
551
  /** Updates {@link positionNames} and calculates {@link startPosition} and {@link endPosition}.
581
552
  */
582
553
  private updatePositions(): void {
583
- if (!this.seqCol)
584
- return;
554
+ if (!this.seqCol) return;
585
555
 
586
556
  const dfFilter = this.filterSource === FilterSources.Filtered ? this.dataFrame.filter :
587
557
  this.dataFrame.selection;
@@ -603,68 +573,191 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
603
573
  this.endPosition = (this.endPositionName && this.positionNames &&
604
574
  this.positionNames.includes(this.endPositionName)) ?
605
575
  this.positionNames.indexOf(this.endPositionName) : (maxLength - 1);
606
- }
607
576
 
608
- private get widthArea() {
609
- return this.Length * this.positionWidth / window.devicePixelRatio;
577
+ this.render(WlRenderLevel.Freqs, 'updatePositions').then(() => {});
610
578
  }
611
579
 
612
- private get heightArea() {
613
- return Math.min(this.maxHeight, Math.max(this.minHeight, this.root.clientHeight));
580
+ setSliderVisibility(visible: boolean): void {
581
+ if (visible) {
582
+ this.slider.root.style.display = 'inherit';
583
+ this.visibleSlider = true;
584
+ } else {
585
+ this.slider.root.style.display = 'none';
586
+ this.visibleSlider = false;
587
+ }
614
588
  }
615
589
 
616
- private get xScale() {
617
- return this.widthArea > 0 ? (this.root.clientWidth - this.Length * this.positionMarginValue) / this.widthArea : 0;
618
- }
590
+ /** Updates {@link host}, {@link canvas}, {@link slider}.
591
+ * Calls {@link render} with {@link WlRenderLevel.Layout}
592
+ */
593
+ private calcLayout(dpr: number): void {
594
+ if (!this.host || !this.canvas || !this.slider) return;
595
+ if (this.root.clientHeight === 0 || this.root.clientWidth === 0) return;
596
+
597
+ this.host.classList.remove('bio-wl-fixWidth', 'bio-wl-fitArea');
598
+ this.canvas.classList.remove('bio-wl-fixWidth', 'bio-wl-fitArea');
619
599
 
620
- private get yScale() {
621
- return this.root.clientHeight / this.heightArea;
600
+ this._positionWidth = this.positionWidth;
601
+ this._positionMargin = this.positionMargin;
602
+ this._positionWidthWithMargin = this._positionWidth + this.positionMarginValue;
603
+
604
+ if (this.fixWidth)
605
+ this.calcLayoutFixWidth(dpr);
606
+ else if (this.fitArea)
607
+ this.calcLayoutFitArea(dpr);
608
+ else
609
+ this.calcLayoutNoFitArea(dpr);
622
610
  }
623
611
 
624
- private checkIsHideSlider(): boolean {
625
- let showSliderWithFitArea = true;
626
- const minScale = Math.min(this.xScale, this.yScale);
612
+ /** */
613
+ private calcLayoutFixWidth(dpr: number): void {
614
+ if (!this.host || !this.canvas || !this.slider) return; // for es-lint
627
615
 
628
- if (((minScale == this.xScale) || (minScale <= 1)) && (this.fitArea))
629
- showSliderWithFitArea = false;
616
+ this.host.classList.add('bio-wl-fixWidth');
617
+ this.canvas.classList.add('bio-wl-fitArea');
630
618
 
631
- return ((this.fixWidth || Math.ceil(this.canvas.width / this.positionWidthWithMargin) >= this.Length) ||
632
- (showSliderWithFitArea));
619
+ const areaWidth: number = this._positionWidthWithMargin * this.Length;
620
+ const areaHeight: number = Math.min(Math.max(this.minHeight, this.root.clientHeight), this.maxHeight);
621
+
622
+ this.host.style.justifyContent = HorizontalAlignments.LEFT;
623
+ this.host.style.removeProperty('margin-left');
624
+ this.host.style.removeProperty('margin-top');
625
+
626
+ this.host.style.width = this.canvas.style.width = `${areaWidth}px`;
627
+ this.host.style.height = this.canvas.style.height = `${areaHeight}px`;
628
+ this.host.style.left = this.canvas.style.left = '0';
629
+ this.host.style.top = this.canvas.style.top = '0';
630
+ this.host.style.setProperty('overflow', 'hidden', 'important');
631
+
632
+ this.slider.root.style.display = 'none';
633
+
634
+ this.slider.setValues(0, this.Length - 1, 0, this.Length - 1);
635
+
636
+ this.canvas.width = areaWidth * dpr;
637
+ this.canvas.height = areaHeight * dpr;
633
638
  }
634
639
 
635
- setSliderVisibility(visible: boolean): void {
636
- if (visible) {
637
- this.slider.root.style.display = 'inherit';
638
- this.visibleSlider = true;
640
+ private calcLayoutNoFitArea(dpr: number): void {
641
+ if (!this.host || !this.canvas || !this.slider) return; // for es-lint
642
+
643
+ const areaWidth: number = this._positionWidthWithMargin * this.Length;
644
+ const areaHeight: number = Math.min(Math.max(this.minHeight, this.root.clientHeight), this.maxHeight);
645
+
646
+ const height = areaHeight;
647
+ const width = Math.min(this.root.clientWidth, areaWidth);
648
+
649
+ this.canvas.style.width = `${width}px`;
650
+ this.canvas.style.height = `${height}px`;
651
+ this.host.style.width = `${width}px`;
652
+ this.host.style.height = `${this.root.clientHeight}px`;
653
+
654
+ // host style flex-direction: row;
655
+ this.host.style.justifyContent = this.horizontalAlignment;
656
+ this.host.style.alignContent =
657
+ this.verticalAlignment === VerticalAlignments.TOP ? 'start' :
658
+ this.verticalAlignment === VerticalAlignments.MIDDLE ? 'center' :
659
+ this.verticalAlignment === VerticalAlignments.BOTTOM ? 'end' :
660
+ 'inherit';
661
+
662
+ if (this.root.clientHeight < this.minHeight) {
663
+ this.host.style.alignContent = 'start'; /* For vertical scroller to work properly */
664
+ this.host.style.width = `${width + 6}px`; /* */
665
+ }
666
+
667
+ this.host.style.width = `${this.host}px`;
668
+
669
+ const sliderVisibility = areaWidth > width;
670
+ this.setSliderVisibility(sliderVisibility);
671
+ if (sliderVisibility) {
672
+ this.slider.root.style.removeProperty('display');
673
+ this.host.style.justifyContent = 'left'; /* For horizontal scroller to prevent */
674
+ this.host.style.height = `${this.root.clientHeight - this.slider.root.offsetHeight}px`;
675
+ this.slider.root.style.top = `${this.host.offsetHeight}px`;
676
+
677
+ let newMin = Math.min(Math.max(0, this.slider.min), this.Length - 0.001);
678
+ let newMax = Math.min(Math.max(0, this.slider.max), this.Length - 0.001);
679
+
680
+ const visibleLength = this.root.clientWidth / this._positionWidthWithMargin;
681
+ newMax = Math.min(Math.max(newMin, 0) + visibleLength, this.Length - 0.001);
682
+ newMin = Math.max(0, Math.min(newMax, this.Length - 0.001) - visibleLength);
683
+
684
+ this.slider.setValues(0, this.Length - 0.001, newMin, newMax);
639
685
  } else {
640
- this.slider.root.style.display = 'none';
641
- this.visibleSlider = false;
686
+ //
687
+ this.slider.setValues(0, this.Length - 0.001, 0, this.Length - 0.001);
642
688
  }
689
+
690
+ this.canvas.width = width * dpr;
691
+ this.canvas.height = height * dpr;
643
692
  }
644
693
 
645
- /** Updates {@link slider}, needed to set slider options and to update slider position. */
646
- private updateSlider(): void {
647
- if (this.checkIsHideSlider())
648
- this.setSliderVisibility(false);
649
- else
650
- this.setSliderVisibility(true);
651
-
652
- if ((this.slider != null) && (this.canvas != null)) {
653
- const diffEndScrollAndSliderMin = Math.max(0,
654
- Math.floor(this.slider.min + this.canvas.width / this.positionWidthWithMargin) - this.Length);
655
- let newMin = Math.floor(this.slider.min - diffEndScrollAndSliderMin);
656
- let newMax = Math.floor(this.slider.min - diffEndScrollAndSliderMin) +
657
- Math.floor(this.canvas.width / this.positionWidthWithMargin);
658
- if (this.checkIsHideSlider()) {
659
- newMin = 0;
660
- newMax = Math.max(newMin, this.Length - 1);
661
- }
662
- this.turnOfResizeForOneSetValue = true;
663
- this.slider.setValues(0, this.Length,
664
- newMin, newMax);
694
+ private calcLayoutFitArea(dpr: number): void {
695
+ if (!this.host || !this.canvas || !this.slider) return; // for es-lint
696
+
697
+ const originalAreaWidthWoMargins: number = this._positionWidth * this.Length;
698
+ const originalAreaHeight: number = Math.min(Math.max(this.minHeight, this.root.clientHeight), this.maxHeight);
699
+
700
+ // TODO: scale
701
+ const xScale = originalAreaWidthWoMargins > 0 ?
702
+ (this.root.clientWidth - this.positionMarginValue * this.Length) / originalAreaWidthWoMargins : 0;
703
+ const yScale = this.root.clientHeight / originalAreaHeight;
704
+ const scale = Math.max(1, Math.min(xScale, yScale));
705
+
706
+ this._positionWidth = this.positionWidth * scale;
707
+ // Do not scale this._positionMargin
708
+ this._positionWidthWithMargin = this._positionWidth + this.positionMarginValue;
709
+ const areaWidth = (this._positionWidth + this.positionMarginValue) * this.Length;
710
+ const areaHeight = scale * originalAreaHeight;
711
+
712
+ const height = areaHeight;
713
+ const width = Math.min(this.root.clientWidth, areaWidth);
714
+
715
+ this.canvas.style.width = `${width}px`;
716
+ this.canvas.style.height = `${height}px`;
717
+ this.host.style.width = `${width}px`;
718
+ this.host.style.height = `${this.root.clientHeight}px`;
719
+
720
+ // host style flex-direction: row;
721
+ this.host.style.justifyContent = this.horizontalAlignment;
722
+ this.host.style.alignContent =
723
+ this.verticalAlignment === VerticalAlignments.TOP ? 'start' :
724
+ this.verticalAlignment === VerticalAlignments.MIDDLE ? 'center' :
725
+ this.verticalAlignment === VerticalAlignments.BOTTOM ? 'end' :
726
+ 'inherit';
727
+
728
+ if (this.root.clientHeight < this.minHeight) {
729
+ this.host.style.alignContent = 'start'; /* For vertical scroller to work properly */
730
+ this.host.style.width = `${width + 6}px`; /* */
665
731
  }
732
+
733
+ this.host.style.width = `${this.host}px`;
734
+
735
+ const sliderVisibility = areaWidth > width;
736
+ this.setSliderVisibility(sliderVisibility);
737
+ if (sliderVisibility) {
738
+ this.slider.root.style.removeProperty('display');
739
+ this.host.style.justifyContent = 'left'; /* For horizontal scroller to prevent */
740
+ this.host.style.height = `${this.root.clientHeight - this.slider.root.offsetHeight}px`;
741
+ this.slider.root.style.top = `${this.host.offsetHeight}px`;
742
+
743
+ let newMin = Math.min(Math.max(0, this.slider.min), this.Length - 0.001);
744
+ let newMax = Math.min(Math.max(0, this.slider.max), this.Length - 0.001);
745
+
746
+ const visibleLength = this.root.clientWidth / this._positionWidthWithMargin;
747
+ newMax = Math.min(Math.max(newMin, 0) + visibleLength, this.Length - 0.001);
748
+ newMin = Math.max(0, Math.min(newMax, this.Length - 0.001) - visibleLength);
749
+
750
+ this.slider.setValues(0, this.Length - 0.001, newMin, newMax);
751
+ } else {
752
+ //
753
+ this.slider.setValues(0, this.Length - 0.001, 0, this.Length - 0.001);
754
+ }
755
+
756
+ this.canvas.width = width * dpr;
757
+ this.canvas.height = height * dpr;
666
758
  }
667
759
 
760
+
668
761
  /** Handler of property change events.
669
762
  * @param {DG.Property} property - property which was changed.
670
763
  */
@@ -672,39 +765,35 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
672
765
  super.onPropertyChanged(property);
673
766
 
674
767
  switch (property.name) {
768
+ case PROPS.sequenceColumnName:
769
+ this.updateSeqCol();
770
+ break;
675
771
  case PROPS.sequenceColumnName:
676
772
  case PROPS.startPositionName:
677
773
  case PROPS.endPositionName:
678
774
  case PROPS.filterSource:
679
- this.updateSeqCol();
680
- break;
681
- case PROPS.positionWidth:
682
- this._positionWidth = this.positionWidth;
683
- this.updateSlider();
684
- break;
685
- case PROPS.fixWidth:
686
- case PROPS.fitArea:
687
- case PROPS.positionMargin:
688
- this.updateSlider();
689
- break;
690
- case PROPS.showPositionLabels:
691
775
  case PROPS.shrinkEmptyTail:
692
776
  case PROPS.skipEmptyPositions:
777
+ case PROPS.positionHeight:
693
778
  this.updatePositions();
694
779
  break;
695
- }
696
-
697
- switch (property.name) {
780
+ // this.positionWidth obtains a new value
781
+ // this.updateSlider updates this._positionWidth
782
+ case PROPS.minHeight:
783
+ case PROPS.maxHeight:
784
+ case PROPS.positionWidth:
785
+ case PROPS.showPositionLabels:
698
786
  case PROPS.fixWidth:
699
787
  case PROPS.fitArea:
700
- case PROPS.showPositionLabels:
701
- case PROPS.positionWidth:
788
+ case PROPS.horizontalAlignment:
789
+ case PROPS.verticalAlignment:
702
790
  case PROPS.positionMargin:
703
- this.render(RecalcLevel.Layout, 'onPropertyChanged');
791
+ case PROPS.positionMarginState:
792
+ this.render(WlRenderLevel.Layout, `onPropertyChanged(${property.name})`).then(() => {});
704
793
  break;
705
794
 
706
- default:
707
- this.render(RecalcLevel.Freqs, 'onPropertyChanged');
795
+ case PROPS.backgroundColor:
796
+ this.render(WlRenderLevel.Render, `onPropertyChanged(${property.name})`).then(() => {});
708
797
  break;
709
798
  }
710
799
  }
@@ -731,11 +820,20 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
731
820
 
732
821
  public get onSizeChanged(): Observable<void> { return this._onSizeChanged; }
733
822
 
823
+ private _onFreqsCalculated: Subject<void> = new Subject<void>();
824
+
825
+ /** Allows {@link VdRegionsViewer} to fit enclosed WebLogo viewers */
826
+ public get onFreqsCalculated(): Observable<void> { return this._onFreqsCalculated; }
827
+
828
+ /** Allows {@link VdRegionsViewer} to fit enclosed WebLogo viewers */
829
+ private _onLayoutCalculated: Subject<void> = new Subject<void>();
830
+ public get onLayoutCalculated(): Observable<void> { return this._onLayoutCalculated; }
831
+
734
832
  // -- Routines --
735
833
 
736
834
  getMonomer(p: DG.Point, dpr: number): [number, string | null, PositionMonomerInfo | null] {
737
- const calculatedX = p.x + this.firstVisibleIndex * this.positionWidthWithMargin * dpr;
738
- const jPos = Math.floor(p.x / (this.positionWidthWithMargin * dpr) + this.firstVisibleIndex);
835
+ const calculatedX = p.x;
836
+ const jPos = Math.floor(p.x / (this._positionWidthWithMargin * dpr) + Math.floor(this.slider.min));
739
837
  const position: PositionInfo = this.positions[jPos];
740
838
 
741
839
  if (position === undefined)
@@ -767,12 +865,11 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
767
865
  }
768
866
  }
769
867
 
770
- private renderRequested: boolean = false;
771
868
  /** default value of RecalcLevel.Freqs is for recalc from the scratch at the beginning */
772
- private recalcLevelRequested: RecalcLevel = RecalcLevel.Freqs;
869
+ private renderLevelRequested: WlRenderLevel = WlRenderLevel.Freqs;
773
870
 
774
871
  /** Renders requested repeatedly will be performed once on window.requestAnimationFrame() */
775
- render(recalcLevel: RecalcLevel, reason: string): void {
872
+ render(recalcLevel: WlRenderLevel, reason: string): Promise<void> {
776
873
  _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>` +
777
874
  `.render( recalcLevel=${recalcLevel}, reason='${reason}' )`);
778
875
 
@@ -799,7 +896,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
799
896
  const splitted = this.unitsHandler.splitted;
800
897
  for (let rowI = 0; rowI < dfRowCount; ++rowI) {
801
898
  if (dfFilter.get(rowI)) {
802
- const seqMList: string[] = splitted[rowI];
899
+ const seqMList: ISeqSplitted = splitted[rowI];
803
900
  for (let jPos = 0; jPos < length; ++jPos) {
804
901
  const m: string = seqMList[this.startPosition + jPos] || '-';
805
902
  const pmInfo = this.positions[jPos].getFreq(m);
@@ -815,30 +912,32 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
815
912
  }
816
913
  //#endregion
817
914
  this._removeEmptyPositions();
915
+ this._onFreqsCalculated.next();
818
916
  };
819
917
 
820
918
  /** Calculate layout of monomers on screen (canvas) based on freqs, required to handle mouse events */
821
919
  const calculateLayoutInt = (dpr: number, positionLabelsHeight: number): void => {
822
920
  _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.render.calculateLayoutInt(), start `);
823
921
 
824
- const length = this.positions.length;
825
- this.calcSize();
922
+ this.calcLayout(dpr);
826
923
  const absoluteMaxHeight = this.canvas.height - positionLabelsHeight * dpr;
827
924
  const alphabetSize = this.getAlphabetSize();
828
925
  if ((this.positionHeight == PositionHeight.Entropy) && (alphabetSize == null))
829
926
  grok.shell.error('WebLogo: alphabet is undefined.');
830
927
  const alphabetSizeLog = Math.log2(alphabetSize);
831
928
 
832
- for (let jPos = 0; jPos < length; jPos++) {
833
- this.positions[jPos].calcScreen(jPos, absoluteMaxHeight, this.positionHeight as PositionHeight,
834
- alphabetSizeLog, this.positionWidthWithMargin, this._positionWidth, dpr, positionLabelsHeight);
929
+ for (let jPos = Math.floor(this.slider.min); jPos <= Math.floor(this.slider.max); ++jPos) {
930
+ this.positions[jPos].calcScreen(jPos, this.slider.min, absoluteMaxHeight, this.positionHeight,
931
+ alphabetSizeLog, this._positionWidthWithMargin, this._positionWidth, dpr, positionLabelsHeight);
835
932
  }
933
+ _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.render.calculateLayoutInt(), end `);
934
+ this._onLayoutCalculated.next();
836
935
  };
837
936
 
838
937
  /** Render WebLogo sensitive to changes in params of rendering
839
- *@param {boolean} recalcFreqs - indicates that need to recalculate data for rendering
938
+ *@param {WlRenderLevel} recalcLevel - indicates that need to recalculate data for rendering
840
939
  */
841
- const renderInt = (recalcLevel: RecalcLevel) => {
940
+ const renderInt = (recalcLevel: WlRenderLevel) => {
842
941
  _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.render.renderInt( recalcLevel=${recalcLevel} ), ` +
843
942
  `start `);
844
943
  if (this.msgHost) {
@@ -862,8 +961,8 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
862
961
  const dpr: number = window.devicePixelRatio;
863
962
  /** 0 is for no position labels */
864
963
  const positionLabelsHeight = this.showPositionLabels ? POSITION_LABELS_HEIGHT : 0;
865
- if (recalcLevel >= RecalcLevel.Freqs) calculateFreqsInt();
866
- if (recalcLevel >= RecalcLevel.Layout) calculateLayoutInt(window.devicePixelRatio, positionLabelsHeight);
964
+ if (recalcLevel >= WlRenderLevel.Freqs) calculateFreqsInt();
965
+ if (recalcLevel >= WlRenderLevel.Layout) calculateLayoutInt(window.devicePixelRatio, positionLabelsHeight);
867
966
 
868
967
  const length: number = this.Length;
869
968
  g.resetTransform();
@@ -871,10 +970,6 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
871
970
  g.fillRect(0, 0, this.canvas.width, this.canvas.height);
872
971
  g.textBaseline = this.textBaseline;
873
972
 
874
- const maxCountOfRowsRendered = this.countOfRenderPositions + 1;
875
- const firstVisibleIndex = (this.visibleSlider) ? Math.floor(this.slider.min) : 0;
876
- const lastVisibleIndex = Math.min(length, firstVisibleIndex + maxCountOfRowsRendered);
877
-
878
973
  //#region Plot positionNames
879
974
  const positionFontSize = 10 * dpr;
880
975
  g.resetTransform();
@@ -886,139 +981,43 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
886
981
  (this._positionWidth * dpr - 2) / posNameMaxWidth;
887
982
 
888
983
  if (positionLabelsHeight > 0) {
889
- renderPositionLabels(g, dpr, hScale, this.positionWidthWithMargin, this._positionWidth,
890
- this.positions, this.firstVisibleIndex, lastVisibleIndex);
984
+ renderPositionLabels(g, dpr, hScale, this._positionWidthWithMargin, this._positionWidth,
985
+ this.positions, this.slider.min, this.slider.max);
891
986
  }
892
987
  //#endregion Plot positionNames
893
988
  const fontStyle = '16px Roboto, Roboto Local, sans-serif';
894
989
  // Hacks to scale uppercase characters to target rectangle
895
990
  const uppercaseLetterAscent = 0.25;
896
991
  const uppercaseLetterHeight = 12.2;
897
- for (let jPos = this.firstVisibleIndex; jPos < lastVisibleIndex; jPos++) {
992
+ for (let jPos = Math.floor(this.slider.min); jPos <= Math.floor(this.slider.max); jPos++) {
898
993
  this.positions[jPos].render(g, fontStyle, uppercaseLetterAscent, uppercaseLetterHeight,
899
- this.positionWidthWithMargin, firstVisibleIndex, this.cp);
994
+ /* this._positionWidthWithMargin, firstVisiblePosIdx,*/ this.cp);
900
995
  }
901
996
 
902
997
  _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.render.renderInt( recalcLevel=${recalcLevel} ), end`);
903
998
  };
904
999
 
905
- this.recalcLevelRequested = Math.max(this.recalcLevelRequested, recalcLevel);
906
- if (!this.renderRequested) {
907
- this.renderRequested = true;
908
- // requestAnimationFrame callback will be executed after this.render()
909
- switch (this.recalcLevelRequested) {
910
- case RecalcLevel.Freqs:
911
- /* Avoiding [Violation] 'requestAnimationFrame' handler took too much */
912
- window.setTimeout(() => {
913
- renderInt(this.recalcLevelRequested);
914
- this.recalcLevelRequested = RecalcLevel.None;
915
- this.renderRequested = false;
916
- }, 0 /* next event cycle */);
917
- break;
918
-
919
- case RecalcLevel.Layout:
920
- case RecalcLevel.None:
921
- window.requestAnimationFrame((time: number) => {
922
- renderInt(this.recalcLevelRequested);
923
- this.recalcLevelRequested = RecalcLevel.None;
924
- this.renderRequested = false;
925
- });
926
- break;
927
- }
928
- }
1000
+ this.renderLevelRequested = Math.max(this.renderLevelRequested, recalcLevel);
1001
+ return new Promise<void>((resolve, reject) => {
1002
+ window.setTimeout(() => {
1003
+ try {
1004
+ if (this.renderLevelRequested > WlRenderLevel.None) {
1005
+ try {
1006
+ renderInt(this.renderLevelRequested);
1007
+ } finally {
1008
+ this.renderLevelRequested = WlRenderLevel.None;
1009
+ }
1010
+ }
1011
+ } catch (err: any) {
1012
+ reject(err);
1013
+ }
1014
+ }, 0 /* next event cycle */);
1015
+ });
929
1016
  }
930
1017
 
931
1018
  private _lastWidth: number;
932
1019
  private _lastHeight: number;
933
1020
 
934
- /** Calculate canvas size an positionWidth and updates properties */
935
- private calcSize() {
936
- if (!this.host) return;
937
-
938
- const dpr: number = window.devicePixelRatio;
939
-
940
- let width: number = this.widthArea;
941
- let height = this.heightArea;
942
-
943
- if ((this.fitArea) && (!this.visibleSlider)) {
944
- const scale = Math.max(1, Math.min(this.xScale, this.yScale));
945
- width = width * scale;
946
- height = height * scale;
947
- this._positionWidth = this.positionWidth * scale;
948
- }
949
-
950
- width = this.Length * this.positionWidthWithMargin;
951
-
952
- this.canvas.width = this.root.clientWidth * dpr;
953
- this.canvas.style.width = `${this.root.clientWidth}px`;
954
-
955
- // const canvasHeight: number = width > this.root.clientWidth ? height - 8 : height;
956
- this.host.style.setProperty('height', `${height}px`);
957
- const canvasHeight: number = this.host.clientHeight;
958
- this.canvas.height = canvasHeight * dpr;
959
-
960
- // Adjust host and root width
961
- if (this.fixWidth) {
962
- // full width for canvas host and root
963
- this.root.style.width = this.host.style.width = `${width}px`;
964
- this.root.style.height = `${height}px`;
965
- this.root.style.overflow = 'hidden';
966
- this.host.style.setProperty('overflow-y', 'hidden', 'important');
967
- } else {
968
- // allow scroll canvas in root
969
- this.root.style.width = this.host.style.width = '100%';
970
- this.host.style.overflowX = 'auto!important';
971
- this.host.style.setProperty('text-align', this.horizontalAlignment);
972
-
973
- const sliderHeight = this.visibleSlider ? 10 : 0;
974
-
975
- // vertical alignment
976
- let hostTopMargin = 0;
977
- switch (this.verticalAlignment) {
978
- case 'top':
979
- hostTopMargin = 0;
980
- break;
981
- case 'middle':
982
- hostTopMargin = Math.max(0, (this.root.clientHeight - height) / 2);
983
- break;
984
- case 'bottom':
985
- hostTopMargin = Math.max(0, this.root.clientHeight - height - sliderHeight);
986
- break;
987
- }
988
- // horizontal alignment
989
- let hostLeftMargin = 0;
990
- switch (this.horizontalAlignment) {
991
- case HorizontalAlignments.LEFT:
992
- hostLeftMargin = 0;
993
- break;
994
- case HorizontalAlignments.CENTER:
995
- hostLeftMargin = Math.max(0, (this.root.clientWidth - width) / 2);
996
- break;
997
- case HorizontalAlignments.RIGHT:
998
- hostLeftMargin = Math.max(0, this.root.clientWidth - width);
999
- break;
1000
- }
1001
- this.host.style.setProperty('margin-top', `${hostTopMargin}px`, 'important');
1002
- this.host.style.setProperty('margin-left', `${hostLeftMargin}px`, 'important');
1003
- if (this.slider != null)
1004
- this.slider.root.style.setProperty('margin-top', `${hostTopMargin + canvasHeight}px`, 'important');
1005
-
1006
-
1007
- if (this.root.clientHeight <= height) {
1008
- this.host.style.setProperty('height', `${this.root.clientHeight}px`);
1009
- this.host.style.setProperty('overflow-y', null);
1010
- } else {
1011
- this.host.style.setProperty('overflow-y', 'hidden', 'important');
1012
- }
1013
- }
1014
-
1015
- if (this._lastWidth !== this.root.clientWidth && this._lastHeight !== this.root.clientHeight) {
1016
- this._lastWidth = this.root.clientWidth;
1017
- this._lastHeight = this.root.clientHeight;
1018
- this._onSizeChanged.next();
1019
- }
1020
- }
1021
-
1022
1021
  public getAlphabetSize(): number {
1023
1022
  return this.unitsHandler?.getAlphabetSize() ?? 0;
1024
1023
  }
@@ -1026,20 +1025,17 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1026
1025
  // -- Handle events --
1027
1026
 
1028
1027
  private sliderOnValuesChanged(_value: any): void {
1029
- if ((this.host == null)) return;
1030
-
1028
+ // if ((this.host == null)) return;
1029
+ // const dpr = window.devicePixelRatio;
1030
+ //
1031
1031
  try {
1032
- /* Resize slider if we can resize do that */
1033
- if ((this.allowResize) && (!this.turnOfResizeForOneSetValue) &&
1034
- (this.visibleSlider)) {
1035
- const countOfPositions = Math.ceil(this.slider.max - this.slider.min);
1036
- const calculatedWidth = (this.canvas.width / countOfPositions) - this.positionMarginValue;
1037
- // saving positionWidth value global (even if slider is not visible)
1038
- this.positionWidth = calculatedWidth;
1039
- this._positionWidth = calculatedWidth;
1040
- }
1041
- this.turnOfResizeForOneSetValue = false;
1042
- this.render(RecalcLevel.Layout, 'sliderOnValuesChanged');
1032
+ const val = {
1033
+ minRange: this.slider.minRange,
1034
+ min: this.slider.min, max: this.slider.max,
1035
+ maxRange: this.slider.maxRange
1036
+ };
1037
+ _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.sliderOnValuesChanged( ${JSON.stringify(val)} ), start`);
1038
+ this.render(WlRenderLevel.Layout, 'sliderOnValuesChanged').then(() => {});
1043
1039
  } catch (err: any) {
1044
1040
  const errMsg = errorToConsole(err);
1045
1041
  _package.logger.error('Bio: WebLogoViewer<${this.viewerId}>.sliderOnValuesChanged() error:\n' + errMsg);
@@ -1052,7 +1048,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1052
1048
  try {
1053
1049
  this.updatePositions();
1054
1050
  if (this.filterSource === FilterSources.Filtered)
1055
- this.render(RecalcLevel.Freqs, 'dataFrameFilterOnChanged');
1051
+ this.render(WlRenderLevel.Freqs, 'dataFrameFilterOnChanged').then(() => {});
1056
1052
  } catch (err: any) {
1057
1053
  const errMsg = errorToConsole(err);
1058
1054
  _package.logger.error('Bio: WebLogoViewer<${this.viewerId}>.dataFrameFilterOnChanged() error:\n' + errMsg);
@@ -1064,7 +1060,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1064
1060
  _package.logger.debug('Bio: WebLogoViewer<${this.viewerId}>.dataFrameSelectionOnChanged()');
1065
1061
  try {
1066
1062
  if (this.filterSource === FilterSources.Selected)
1067
- this.render(RecalcLevel.Freqs, 'dataFrameSelectionOnChanged');
1063
+ this.render(WlRenderLevel.Freqs, 'dataFrameSelectionOnChanged').then(() => {});
1068
1064
  } catch (err: any) {
1069
1065
  const errMsg = errorToConsole(err);
1070
1066
  _package.logger.error('Bio: WebLogoViewer<${this.viewerId}>.dataFrameSelectionOnChanged() error:\n' + errMsg);
@@ -1130,10 +1126,11 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1130
1126
  }
1131
1127
 
1132
1128
  private canvasOnWheel(e: WheelEvent) {
1129
+ const dpr = window.devicePixelRatio;
1133
1130
  try {
1134
- if (!this.visibleSlider)
1135
- return;
1136
- const countOfScrollPositions = (e.deltaY / 100) * Math.max(Math.floor((this.countOfRenderPositions) / 2), 1);
1131
+ if (!this.visibleSlider) return;
1132
+ const visibleLength = this.canvas.width / (this._positionWidthWithMargin * dpr);
1133
+ const countOfScrollPositions = (e.deltaY / 100) * Math.max(Math.floor((visibleLength) / 5), 1);
1137
1134
  this.slider.scrollBy(this.slider.min + countOfScrollPositions);
1138
1135
  } catch (err: any) {
1139
1136
  const errMsg = errorToConsole(err);
@@ -1145,14 +1142,14 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1145
1142
 
1146
1143
  function renderPositionLabels(g: CanvasRenderingContext2D,
1147
1144
  dpr: number, hScale: number, positionWidthWithMargin: number, positionWidth: number,
1148
- positions: PositionInfo[], firstVisibleIndex: number, lastVisibleIndex: number): void {
1149
- for (let jPos = firstVisibleIndex; jPos < lastVisibleIndex; jPos++) {
1145
+ positions: PositionInfo[], firstVisiblePosIdx: number, lastVisiblePosIdx: number
1146
+ ): void {
1147
+ for (let jPos = Math.floor(firstVisiblePosIdx); jPos <= Math.floor(lastVisiblePosIdx); jPos++) {
1150
1148
  const pos: PositionInfo = positions[jPos];
1151
1149
  g.resetTransform();
1152
1150
  g.setTransform(
1153
- hScale, 0, 0, 1,
1154
- jPos * positionWidthWithMargin * dpr + positionWidth * dpr / 2 -
1155
- positionWidthWithMargin * firstVisibleIndex, 0);
1151
+ hScale, 0, 0,
1152
+ 1, (jPos - firstVisiblePosIdx) * positionWidthWithMargin * dpr + positionWidth * dpr / 2, 0);
1156
1153
  g.fillText(pos.label, 0, 1);
1157
1154
  }
1158
1155
  }
@@ -1160,7 +1157,7 @@ function renderPositionLabels(g: CanvasRenderingContext2D,
1160
1157
  export function checkSeqForMonomerAtPos(
1161
1158
  df: DG.DataFrame, unitsHandler: UnitsHandler, filter: DG.BitSet, rowI: number, monomer: string, at: PositionInfo,
1162
1159
  ): boolean {
1163
- const seqMList: string[] = unitsHandler.splitted[rowI];
1160
+ const seqMList: ISeqSplitted = unitsHandler.splitted[rowI];
1164
1161
  const seqM = at.pos < seqMList.length ? seqMList[at.pos] : null;
1165
1162
  return ((seqM === monomer) || (seqM === '' && monomer === '-'));
1166
1163
  }
@@ -1171,7 +1168,7 @@ export function countForMonomerAtPosition(
1171
1168
  let count = 0;
1172
1169
  let rowI = -1;
1173
1170
  while ((rowI = filter.findNext(rowI, true)) != -1) {
1174
- const seqMList: string[] = uh.splitted[rowI];
1171
+ const seqMList: ISeqSplitted = uh.splitted[rowI];
1175
1172
  const seqMPos: number = at.pos;
1176
1173
  const seqM: string | null = seqMPos < seqMList.length ? seqMList[seqMPos] : null;
1177
1174
  if (seqM === monomer) count++;