@datagrok/bio 2.10.28 → 2.11.0

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.
@@ -18,7 +18,9 @@ import {errorToConsole} from '@datagrok-libraries/utils/src/to-console';
18
18
  import {intToHtmlA} from '@datagrok-libraries/utils/src/color';
19
19
  import {ISeqSplitted} from '@datagrok-libraries/bio/src/utils/macromolecule/types';
20
20
 
21
+ import {AggFunc, getAgg} from '../utils/agg';
21
22
  import {errInfo} from '../utils/err-info';
23
+ import {buildCompositionTable} from '../widgets/composition-analysis-widget';
22
24
 
23
25
  import {_package} from '../package';
24
26
 
@@ -43,15 +45,36 @@ DG.Rect.prototype.contains = function(x: number, y: number): boolean {
43
45
 
44
46
  export class PositionMonomerInfo {
45
47
  /** Sequences count with monomer in position */
46
- public count: number;
48
+ public rowCount: number;
49
+ /** Aggregated value */
50
+ public value: number;
51
+ /** Aggregated value shifted to be non-negative */
52
+ public plotValue: number;
53
+
54
+ public valueList: (number | null)[] | null = null;
55
+ public valueIdx: number = 0;
47
56
 
48
57
  /** Remember screen coords rect */
49
58
  public bounds?: DG.Rect;
50
59
 
51
- constructor(count: number = 0, bounds?: DG.Rect) {
52
- this.count = count;
60
+ constructor(rowCount: number = 0, bounds?: DG.Rect) {
61
+ this.value = this.rowCount = rowCount;
53
62
  this.bounds = bounds;
54
63
  }
64
+
65
+ public push(value: number | null): void {
66
+ if (!this.valueList) {
67
+ this.valueList = new Array<number>(this.rowCount);
68
+ this.valueIdx = 0;
69
+ }
70
+ this.valueList[this.valueIdx] = value;
71
+ ++this.valueIdx;
72
+ }
73
+
74
+ public aggregate(aggFunc: AggFunc): void {
75
+ this.value = aggFunc(this.valueList!) ?? 0;
76
+ this.valueList = null; // clear
77
+ }
55
78
  }
56
79
 
57
80
  export class PositionInfo {
@@ -66,8 +89,11 @@ export class PositionInfo {
66
89
 
67
90
  private readonly _freqs: { [m: string]: PositionMonomerInfo };
68
91
 
69
- rowCount: number;
70
- sumForHeightCalc: number;
92
+ sumRowCount: number = 0;
93
+ /** Sum of plot value */
94
+ sumPlotValue: number;
95
+ /** Sum of plot value scaled to alphabet size with pseudo-count correction */
96
+ sumPlotValueForHeight: number;
71
97
 
72
98
  /** freq = {}, rowCount = 0
73
99
  * @param {number} pos Position in sequence
@@ -77,14 +103,14 @@ export class PositionInfo {
77
103
  * @param {number} sumForHeightCalc Sum of all monomer counts for height calculation
78
104
  */
79
105
  constructor(pos: number, name: string, freqs?: { [m: string]: PositionMonomerInfo },
80
- options?: { rowCount?: number, sumForHeightCalc?: number, label?: string }
106
+ options?: { sumRowCount?: number, sumValueForHeight?: number, label?: string }
81
107
  ) {
82
108
  this.pos = pos;
83
109
  this.name = name;
84
110
  this._freqs = freqs ?? {};
85
111
 
86
- if (options?.rowCount) this.rowCount = options.rowCount;
87
- if (options?.sumForHeightCalc) this.sumForHeightCalc = options.sumForHeightCalc;
112
+ if (options?.sumRowCount) this.sumRowCount = options.sumRowCount;
113
+ if (options?.sumValueForHeight) this.sumPlotValue = options.sumValueForHeight;
88
114
  if (options?.label) this._label = options.label;
89
115
  }
90
116
 
@@ -103,61 +129,60 @@ export class PositionInfo {
103
129
  return res;
104
130
  }
105
131
 
106
- getMonomerAt(calculatedX: number, y: number): string | undefined {
107
- const findRes = Object.entries(this._freqs)
108
- .find(([m, pmInfo]) => {
109
- return pmInfo.bounds!.contains(calculatedX, y);
110
- });
111
- return !!findRes ? findRes[0] : undefined;
132
+ /** Calculates {@link agg} aggregation function for all ${link this.freqs} and clears ${} */
133
+ public aggregate(agg: DG.AggregationType): void {
134
+ const aggFunc = getAgg(agg);
135
+ for (const [m, pmi] of Object.entries(this._freqs))
136
+ pmi.aggregate(aggFunc);
137
+ // this.sumValueForHeight will be calculated before drawing
112
138
  }
113
139
 
114
- calcHeights(heightMode: PositionHeight): void {
115
- /*
116
- this.positions[jPos].rowCount = 0;
117
- for (const m in this.positions[jPos].freq)
118
- this.positions[jPos].rowCount += this.positions[jPos].freq[m].count;
119
- if (this.positionHeight == PositionHeight.Entropy) {
120
- this.positions[jPos].sumForHeightCalc = 0;
121
- for (const m in this.positions[jPos].freq) {
122
- const pn = this.positions[jPos].freq[m].count / this.positions[jPos].rowCount;
123
- this.positions[jPos].sumForHeightCalc += -pn * Math.log2(pn);
124
- }
125
- }
126
- /**/
140
+ getMinValue() {
141
+ return Math.min(...Object.values(this._freqs).map((pmi) => pmi.value));
142
+ }
143
+
144
+ calcPlotValue(shiftAggValue: number) {
145
+ for (const pmi of Object.values(this._freqs))
146
+ pmi.plotValue = pmi.value - shiftAggValue;
147
+ }
127
148
 
128
- this.rowCount = 0;
129
- for (const [m, pmInfo] of Object.entries(this._freqs))
130
- this.rowCount += pmInfo.count;
149
+ /** Calculates {@link .sumPlotValue} overall position */
150
+ calcHeights(heightMode: PositionHeight): void {
151
+ this.sumPlotValue = 0;
152
+ for (const pmi of Object.values(this._freqs))
153
+ this.sumPlotValue += pmi.plotValue;
131
154
 
132
- this.sumForHeightCalc = 0;
155
+ this.sumPlotValueForHeight = 0;
133
156
  if (heightMode === PositionHeight.Entropy) {
134
- for (const [m, pmInfo] of Object.entries(this._freqs)) {
135
- const pn = pmInfo.count / this.rowCount;
136
- this.sumForHeightCalc += -pn * Math.log2(pn);
157
+ const freqsSize = Object.keys(this._freqs).length;
158
+ const sumPseudoCount = 0.01 * this.sumPlotValue;
159
+ const pseudoCount = sumPseudoCount / freqsSize;
160
+ for (const pmi of Object.values(this._freqs)) {
161
+ const pn = (pmi.plotValue + pseudoCount) / (this.sumPlotValue + sumPseudoCount);
162
+ this.sumPlotValueForHeight += -pn * Math.log2(pn);
137
163
  }
138
164
  } else if (heightMode === PositionHeight.full) {
139
- for (const [m, pmInfo] of Object.entries(this._freqs)) {
140
- const pn = pmInfo.count / this.rowCount;
141
- this.sumForHeightCalc += pn;
165
+ for (const [_m, pmi] of Object.entries(this._freqs)) {
166
+ const pn = pmi.plotValue / this.sumPlotValue;
167
+ this.sumPlotValueForHeight += pn;
142
168
  }
143
169
  }
144
- const k = 42;
145
170
  }
146
171
 
147
172
  calcScreen(
148
173
  isGap: (m: string) => boolean, posIdx: number, firstVisiblePosIdx: number,
149
174
  absoluteMaxHeight: number, heightMode: PositionHeight, alphabetSizeLog: number,
150
- positionWidthWithMargin: number, positionWidth: number, dpr: number, axisHeight: number
175
+ positionWidthWithMargin: number, positionWidth: number, dpr: number, positionLabelsHeight: number
151
176
  ): void {
152
- const maxHeight = (heightMode == PositionHeight.Entropy) ?
153
- (absoluteMaxHeight * (alphabetSizeLog - (this.sumForHeightCalc)) / alphabetSizeLog) :
177
+ const maxHeight = (heightMode === PositionHeight.Entropy) ?
178
+ (absoluteMaxHeight * (alphabetSizeLog - (this.sumPlotValueForHeight)) / alphabetSizeLog) :
154
179
  absoluteMaxHeight;
155
- let y: number = axisHeight * dpr + (absoluteMaxHeight - maxHeight - 1);
180
+ let y: number = positionLabelsHeight * dpr + (absoluteMaxHeight - maxHeight - 1);
156
181
 
157
182
  const entries = Object.entries(this._freqs)
158
183
  .sort((a, b) => {
159
184
  if (!isGap(a[0]) && !isGap(b[0]))
160
- return b[1].count - a[1].count;
185
+ return b[1].value - a[1].value;
161
186
  else if (isGap(a[0]) && isGap(b[0]))
162
187
  return 0;
163
188
  else if (isGap(a[0]))
@@ -165,12 +190,10 @@ export class PositionInfo {
165
190
  else /* (isGap(b[0])) */
166
191
  return +1;
167
192
  });
168
- for (const entry of entries) {
169
- const pmInfo: PositionMonomerInfo = entry[1];
170
- // const m: string = entry[0];
171
- const h: number = maxHeight * pmInfo.count / this.rowCount;
193
+ for (const [_m, pmi] of entries) {
194
+ const h: number = maxHeight * pmi.plotValue / this.sumPlotValue;
172
195
 
173
- pmInfo.bounds = new DG.Rect(
196
+ pmi.bounds = new DG.Rect(
174
197
  (posIdx - firstVisiblePosIdx) * dpr * positionWidthWithMargin, y,
175
198
  positionWidth * dpr, h);
176
199
  y += h;
@@ -204,6 +227,21 @@ export class PositionInfo {
204
227
  }
205
228
  }
206
229
  }
230
+
231
+ getMonomerAt(calculatedX: number, y: number): string | undefined {
232
+ const findRes = Object.entries(this._freqs)
233
+ .find(([m, pmInfo]) => {
234
+ return pmInfo.bounds!.contains(calculatedX, y);
235
+ });
236
+ return !!findRes ? findRes[0] : undefined;
237
+ }
238
+
239
+ buildCompositionTable(palette: SeqPalette): HTMLTableElement {
240
+ return buildCompositionTable(palette,
241
+ Object.assign({}, ...Object.entries(this._freqs)
242
+ .map(([m, pmi]) => ({[m]: pmi.rowCount})))
243
+ );
244
+ }
207
245
  }
208
246
 
209
247
  export enum PROPS_CATS {
@@ -216,6 +254,8 @@ export enum PROPS_CATS {
216
254
  export enum PROPS {
217
255
  // -- Data --
218
256
  sequenceColumnName = 'sequenceColumnName',
257
+ valueAggrType = 'valueAggrType',
258
+ valueColumnName = 'valueColumnName',
219
259
  startPositionName = 'startPositionName',
220
260
  endPositionName = 'endPositionName',
221
261
  skipEmptySequences = 'skipEmptySequences',
@@ -264,7 +304,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
264
304
  private initialized: boolean = false;
265
305
 
266
306
  // private readonly colorScheme: ColorScheme = ColorSchemes[NucleotidesWebLogo.residuesSet];
267
- protected cp: SeqPalette | null = null;
307
+ protected palette: SeqPalette | null = null;
268
308
 
269
309
  private host?: HTMLDivElement;
270
310
  private msgHost?: HTMLElement;
@@ -282,6 +322,8 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
282
322
  // Viewer's properties (likely they should be public)
283
323
  // -- Data --
284
324
  public sequenceColumnName: string | null;
325
+ public valueAggrType: DG.AggregationType;
326
+ public valueColumnName: string;
285
327
  public skipEmptySequences: boolean;
286
328
  public skipEmptyPositions: boolean;
287
329
 
@@ -348,6 +390,13 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
348
390
  // -- Data --
349
391
  this.sequenceColumnName = this.string(PROPS.sequenceColumnName, defaults.sequenceColumnName,
350
392
  {category: PROPS_CATS.DATA});
393
+ const aggExcludeList = [DG.AGG.KEY, DG.AGG.PIVOT, DG.AGG.MISSING_VALUE_COUNT, DG.AGG.SKEW, DG.AGG.KURT,
394
+ DG.AGG.SELECTED_ROWS_COUNT];
395
+ const aggChoices = Object.values(DG.AGG).filter((agg) => !aggExcludeList.includes(agg));
396
+ this.valueAggrType = this.string(PROPS.valueAggrType, defaults.valueAggrType,
397
+ {category: PROPS_CATS.DATA, choices: aggChoices}) as DG.AggregationType;
398
+ this.valueColumnName = this.string(PROPS.valueColumnName, defaults.valueColumnName,
399
+ {category: PROPS_CATS.DATA});
351
400
  this.startPositionName = this.string(PROPS.startPositionName, defaults.startPositionName,
352
401
  {category: PROPS_CATS.DATA});
353
402
  this.endPositionName = this.string(PROPS.endPositionName, defaults.endPositionName,
@@ -416,6 +465,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
416
465
  }
417
466
 
418
467
  this.updateSeqCol();
468
+ this.updateEditors();
419
469
 
420
470
  if (!this.viewed) {
421
471
  await this.buildView(); //requests rendering
@@ -518,8 +568,14 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
518
568
  this.render(WlRenderLevel.Layout, 'rootOnSizeChanged');
519
569
  }
520
570
 
521
- /** Assigns {@link seqCol} and {@link cp} based on {@link sequenceColumnName} and calls {@link render}().
522
- */
571
+ private updateEditors(): void {
572
+ const valueColumnNameProp = this.props.getProperty(PROPS.valueColumnName);
573
+ valueColumnNameProp.choices = wu(this.dataFrame.columns.numerical)
574
+ .map((col) => col.name).toArray();
575
+ // Set valueColumnNameProp.choices has no effect
576
+ }
577
+
578
+ /** Assigns {@link seqCol} and {@link palette} based on {@link sequenceColumnName} and calls {@link render}(). */
523
579
  private updateSeqCol(): void {
524
580
  if (this.dataFrame) {
525
581
  this.seqCol = this.sequenceColumnName ? this.dataFrame.col(this.sequenceColumnName) : null;
@@ -531,7 +587,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
531
587
  try {
532
588
  this.unitsHandler = UnitsHandler.getOrCreate(this.seqCol);
533
589
 
534
- this.cp = pickUpPalette(this.seqCol);
590
+ this.palette = pickUpPalette(this.seqCol);
535
591
  this.updatePositions();
536
592
  this.error = null;
537
593
  } catch (err: any) {
@@ -545,7 +601,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
545
601
  this.positionLabels = [];
546
602
  this.startPosition = -1;
547
603
  this.endPosition = -1;
548
- this.cp = null;
604
+ this.palette = null;
549
605
  }
550
606
  }
551
607
  }
@@ -793,9 +849,16 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
793
849
  case PROPS.filterSource:
794
850
  case PROPS.shrinkEmptyTail:
795
851
  case PROPS.skipEmptyPositions:
796
- case PROPS.positionHeight:
852
+ case PROPS.positionHeight: {
797
853
  this.updatePositions();
798
854
  break;
855
+ }
856
+
857
+ case PROPS.valueColumnName:
858
+ case PROPS.valueAggrType: {
859
+ this.render(WlRenderLevel.Freqs, `onPropertyChanged(${property.name})`);
860
+ break;
861
+ }
799
862
  // this.positionWidth obtains a new value
800
863
  // this.updateSlider updates this._positionWidth
801
864
  case PROPS.minHeight:
@@ -807,13 +870,15 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
807
870
  case PROPS.horizontalAlignment:
808
871
  case PROPS.verticalAlignment:
809
872
  case PROPS.positionMargin:
810
- case PROPS.positionMarginState:
873
+ case PROPS.positionMarginState: {
811
874
  this.render(WlRenderLevel.Layout, `onPropertyChanged(${property.name})`);
812
875
  break;
876
+ }
813
877
 
814
- case PROPS.backgroundColor:
878
+ case PROPS.backgroundColor: {
815
879
  this.render(WlRenderLevel.Render, `onPropertyChanged(${property.name})`);
816
880
  break;
881
+ }
817
882
  }
818
883
  }
819
884
 
@@ -821,8 +886,6 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
821
886
  public override onTableAttached() {
822
887
  _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.onTableAttached(), `);
823
888
 
824
- // -- Props editors --
825
-
826
889
  super.onTableAttached();
827
890
  this.setData();
828
891
  }
@@ -861,19 +924,19 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
861
924
 
862
925
  // -- Routines --
863
926
 
864
- getMonomer(p: DG.Point, dpr: number): [number, string | null, PositionMonomerInfo | null] {
927
+ getMonomer(p: DG.Point, dpr: number): [PositionInfo | null, string | null, PositionMonomerInfo | null] {
865
928
  const calculatedX = p.x;
866
929
  const jPos = Math.floor(p.x / (this._positionWidthWithMargin * dpr) + Math.floor(this.slider.min));
867
- const position: PositionInfo = this.positions[jPos];
930
+ const pi: PositionInfo = this.positions[jPos];
868
931
 
869
- if (position === undefined)
870
- return [jPos, null, null];
932
+ if (!pi)
933
+ return [null, null, null];
871
934
 
872
- const monomer: string | undefined = position.getMonomerAt(calculatedX, p.y);
935
+ const monomer: string | undefined = pi.getMonomerAt(calculatedX, p.y);
873
936
  if (monomer === undefined)
874
- return [jPos, null, null];
937
+ return [pi, null, null];
875
938
 
876
- return [jPos, monomer, position.getFreq(monomer)];
939
+ return [pi, monomer, pi.getFreq(monomer)];
877
940
  };
878
941
 
879
942
  /** Helper function for rendering
@@ -892,7 +955,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
892
955
  if (this.skipEmptyPositions) {
893
956
  this.positions = wu(this.positions).filter((pi) => {
894
957
  const gapSymbol: string = this.unitsHandler!.defaultGapSymbol;
895
- return !pi.hasMonomer(gapSymbol) || pi.getFreq(gapSymbol).count !== pi.rowCount;
958
+ return !pi.hasMonomer(gapSymbol) || pi.getFreq(gapSymbol).rowCount !== pi.sumRowCount;
896
959
  }).toArray();
897
960
  }
898
961
  }
@@ -937,23 +1000,48 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
937
1000
  const dfFilter = this.getFilter();
938
1001
  const dfRowCount = this.dataFrame.rowCount;
939
1002
  const splitted = this.unitsHandler.splitted;
940
- for (let rowI = 0; rowI < dfRowCount; ++rowI) {
941
- if (dfFilter.get(rowI)) {
942
- const seqMList: ISeqSplitted = splitted[rowI];
943
- for (let jPos = 0; jPos < length; ++jPos) {
1003
+
1004
+ for (let jPos = 0; jPos < length; ++jPos) {
1005
+ // Here we want to build lists of values for every monomer in position jPos
1006
+ for (let rowI = 0; rowI < dfRowCount; ++rowI) {
1007
+ if (dfFilter.get(rowI)) {
1008
+ const seqMList: ISeqSplitted = splitted[rowI];
1009
+ const m: string = seqMList[this.startPosition + jPos] || this.unitsHandler.defaultGapSymbol;
1010
+ const pi = this.positions[jPos];
1011
+ const pmi = pi.getFreq(m);
1012
+ ++pi.sumRowCount;
1013
+ pmi.value = ++pmi.rowCount;
1014
+ }
1015
+ }
1016
+ if (this.valueAggrType === DG.AGG.TOTAL_COUNT) continue;
1017
+
1018
+ // Now we have counts for each monomer in position jPos,
1019
+ // this allows us to allocate list of values
1020
+ let valueCol: DG.Column<number> | null = null;
1021
+ try {
1022
+ valueCol = this.dataFrame.getCol(this.valueColumnName);
1023
+ if (!valueCol.matches('numerical')) valueCol = null;
1024
+ } catch { valueCol = null; }
1025
+ if (!valueCol) continue; // fallback to TOTAL_COUNT
1026
+
1027
+ for (let rowI = 0; rowI < dfRowCount; ++rowI) {
1028
+ if (dfFilter.get(rowI)) { // respect the filter
1029
+ const seqMList: ISeqSplitted = splitted[rowI];
944
1030
  const m: string = seqMList[this.startPosition + jPos] || this.unitsHandler.defaultGapSymbol;
945
- const pmInfo = this.positions[jPos].getFreq(m);
946
- pmInfo.count++;
1031
+ const value: number | null = valueCol.get(rowI);
1032
+ this.positions[jPos].getFreq(m).push(value);
947
1033
  }
948
1034
  }
1035
+ this.positions[jPos].aggregate(this.valueAggrType);
949
1036
  }
950
1037
 
951
- //#region Polish freq counts
952
- for (let jPos = 0; jPos < length; jPos++) {
953
- // delete this.positions[jPos].freq[this.unitsHandler.defaultGapSymbol];
954
- this.positions[jPos].calcHeights(this.positionHeight as PositionHeight);
1038
+ const shiftAggValue: number = this.valueAggrType === DG.AGG.TOTAL_COUNT ? 0 :
1039
+ Math.min(0, Math.min(...this.positions.map((pi) => pi.getMinValue())));
1040
+ for (let jPos = 0; jPos < length; ++jPos) {
1041
+ this.positions[jPos].calcPlotValue(shiftAggValue);
1042
+ this.positions[jPos].calcHeights(this.positionHeight);
955
1043
  }
956
- //#endregion
1044
+
957
1045
  this._removeEmptyPositions();
958
1046
  this._onFreqsCalculated.next();
959
1047
  };
@@ -962,15 +1050,21 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
962
1050
  const calculateLayoutInt = (firstPos: number, lastPos: number, dpr: number, positionLabelsHeight: number): void => {
963
1051
  _package.logger.debug(`Bio: WebLogoViewer<${this.viewerId}>.render.calculateLayoutInt(), start `);
964
1052
 
965
- const absoluteMaxHeight = this.canvas.height - positionLabelsHeight * dpr;
966
- const alphabetSize = this.getAlphabetSize();
967
- if ((this.positionHeight == PositionHeight.Entropy) && (alphabetSize == null))
968
- grok.shell.error('WebLogo: alphabet is undefined.');
969
- const alphabetSizeLog = Math.log2(alphabetSize);
1053
+ const absoluteMaxHeight: number = this.canvas.height - positionLabelsHeight * dpr;
1054
+ let alphabetSizeLog: number;
1055
+ if (this.valueAggrType === DG.AGG.TOTAL_COUNT) {
1056
+ const alphabetSize: number = this.getAlphabetSize();
1057
+ if ((this.positionHeight == PositionHeight.Entropy) && (alphabetSize == null))
1058
+ grok.shell.error('WebLogo: alphabet is undefined.');
1059
+ alphabetSizeLog = Math.log2(alphabetSize);
1060
+ } else {
1061
+ alphabetSizeLog = Math.max(...wu.count(firstPos).takeWhile((jPos) => jPos <= lastPos)
1062
+ .map((jPos) => this.positions[jPos].sumPlotValueForHeight));
1063
+ }
970
1064
 
971
1065
  for (let jPos = firstPos; jPos <= lastPos; ++jPos) {
972
1066
  if (!(jPos in this.positions)) {
973
- console.warn(`Bio: WebLogoViewer<${this.viewerId}>.render.calculateLayoutInt() ` +
1067
+ _package.logger.warning(`Bio: WebLogoViewer<${this.viewerId}>.render.calculateLayoutInt() ` +
974
1068
  `this.positions.length = ${this.positions.length}, jPos = ${jPos}`);
975
1069
  continue;
976
1070
  }
@@ -983,7 +1077,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
983
1077
  };
984
1078
 
985
1079
  if (this.msgHost) {
986
- if (this.seqCol && !this.cp) {
1080
+ if (this.seqCol && !this.palette) {
987
1081
  this.msgHost!.innerText = `Unknown palette (column semType: '${this.seqCol.semType}').`;
988
1082
  this.msgHost!.style.display = '';
989
1083
  } else {
@@ -991,7 +1085,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
991
1085
  }
992
1086
  }
993
1087
 
994
- if (!this.seqCol || !this.dataFrame || !this.cp || this.host == null || this.slider == null)
1088
+ if (!this.seqCol || !this.dataFrame || !this.palette || this.host == null || this.slider == null)
995
1089
  return;
996
1090
 
997
1091
  const dpr: number = window.devicePixelRatio;
@@ -1021,12 +1115,9 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1021
1115
  g.fillStyle = 'black';
1022
1116
  g.textAlign = 'center';
1023
1117
  g.font = `${positionFontSize.toFixed(1)}px Roboto, Roboto Local, sans-serif`;
1024
- const posNameMaxWidth = Math.max(...this.positions.map((pos) => g.measureText(pos.name).width));
1025
- const hScale = posNameMaxWidth < (this._positionWidth * dpr - 2) ? 1 :
1026
- (this._positionWidth * dpr - 2) / posNameMaxWidth;
1027
1118
 
1028
1119
  if (positionLabelsHeight > 0 && this.positions.length > 0) {
1029
- renderPositionLabels(g, dpr, hScale, this._positionWidthWithMargin, this._positionWidth,
1120
+ renderPositionLabels(g, dpr, this._positionWidthWithMargin, this._positionWidth, positionLabelsHeight,
1030
1121
  this.positions, this.slider.min, this.slider.max);
1031
1122
  }
1032
1123
  //#endregion Plot positionNames
@@ -1036,8 +1127,7 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1036
1127
  const uppercaseLetterHeight = 12.2;
1037
1128
  for (let jPos = firstPos; jPos <= lastPos; jPos++) {
1038
1129
  this.positions[jPos].render(g, (m) => { return this.unitsHandler!.isGap(m); },
1039
- fontStyle, uppercaseLetterAscent, uppercaseLetterHeight,
1040
- /* this._positionWidthWithMargin, firstVisiblePosIdx,*/ this.cp);
1130
+ fontStyle, uppercaseLetterAscent, uppercaseLetterHeight, this.palette);
1041
1131
  }
1042
1132
  } finally {
1043
1133
  g.restore();
@@ -1115,23 +1205,31 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1115
1205
  const args = e as MouseEvent;
1116
1206
 
1117
1207
  const cursorP: DG.Point = this.canvas.getCursorPosition(args, dpr);
1118
- const [jPos, monomer] = this.getMonomer(cursorP, dpr);
1119
- // if (jPos != undefined && monomer == undefined) {
1120
- // const preEl = ui.element('pre');
1121
- // preEl.innerHTML = jPos < this.positions.length ?
1122
- // JSON.stringify(this.positions[jPos].freq, undefined, 2) : 'NO jPos';
1123
- // const tooltipEl = ui.div([ui.div(`pos: ${jPos}`), preEl]);
1124
- // ui.tooltip.show(tooltipEl, args.x + 16, args.y + 16);
1125
- // } else
1126
- if (this.dataFrame && this.seqCol && monomer) {
1127
- const atPI: PositionInfo = this.positions[jPos];
1128
- const monomerAtPosSeqCount = countForMonomerAtPosition(
1129
- this.dataFrame, this.unitsHandler!, this.getFilter(), monomer, atPI);
1130
-
1131
- const tooltipEl = ui.div([
1208
+ const [pi, monomer] = this.getMonomer(cursorP, dpr);
1209
+ const positionLabelHeight = this.showPositionLabels ? POSITION_LABELS_HEIGHT * dpr : 0;
1210
+
1211
+ if (pi !== null && monomer === null && 0 <= cursorP.y && cursorP.y <= positionLabelHeight) {
1212
+ // Position tooltip
1213
+
1214
+ const tooltipRows = [ui.divText(`Position ${pi.label}`)];
1215
+ if (this.valueAggrType === DG.AGG.TOTAL_COUNT)
1216
+ tooltipRows.push(pi.buildCompositionTable(this.palette!));
1217
+ const tooltipEl = ui.divV(tooltipRows);
1218
+ ui.tooltip.show(tooltipEl, args.x + 16, args.y + 16);
1219
+ } else if (pi !== null && monomer && this.dataFrame && this.seqCol && this.unitsHandler) {
1220
+ // Monomer at position tooltip
1221
+ // const monomerAtPosSeqCount = countForMonomerAtPosition(
1222
+ // this.dataFrame, this.unitsHandler!, this.getFilter(), monomer, atPI);
1223
+ const pmi = pi.getFreq(monomer);
1224
+
1225
+ const tooltipRows = [
1132
1226
  // ui.div(`pos ${jPos}`),
1133
1227
  ui.div(`${monomer}`),
1134
- ui.div(`${monomerAtPosSeqCount} rows`)]);
1228
+ ui.div(`${pmi.rowCount} rows`)
1229
+ ];
1230
+ if (this.valueAggrType !== DG.AGG.TOTAL_COUNT)
1231
+ tooltipRows.push(ui.div(`${this.valueAggrType}: ${pmi.value.toFixed(3)}`));
1232
+ const tooltipEl = ui.divV(tooltipRows);
1135
1233
  ui.tooltip.show(tooltipEl, args.x + 16, args.y + 16);
1136
1234
  } else {
1137
1235
  ui.tooltip.hide();
@@ -1147,15 +1245,13 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1147
1245
  try {
1148
1246
  const args = e as MouseEvent;
1149
1247
  const dpr: number = window.devicePixelRatio;
1150
- const [jPos, monomer] = this.getMonomer(this.canvas.getCursorPosition(args, dpr), dpr);
1248
+ const [pi, monomer] = this.getMonomer(this.canvas.getCursorPosition(args, dpr), dpr);
1151
1249
 
1152
1250
  // prevents deselect all rows if we miss monomer bounds
1153
- if (this.dataFrame && this.seqCol && this.unitsHandler && monomer) {
1154
- const atPI: PositionInfo = this.positions[jPos];
1155
-
1251
+ if (pi !== null && monomer !== null && this.dataFrame && this.seqCol && this.unitsHandler) {
1156
1252
  // Calculate a new BitSet object for selection to prevent interfering with existing
1157
1253
  const selBS: DG.BitSet = DG.BitSet.create(this.dataFrame.selection.length, (rowI: number) => {
1158
- return checkSeqForMonomerAtPos(this.dataFrame, this.unitsHandler!, this.getFilter(), rowI, monomer, atPI);
1254
+ return checkSeqForMonomerAtPos(this.dataFrame, this.unitsHandler!, this.getFilter(), rowI, monomer, pi);
1159
1255
  });
1160
1256
  this.dataFrame.selection.init((i) => selBS.get(i));
1161
1257
  }
@@ -1182,16 +1278,36 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1182
1278
  }
1183
1279
 
1184
1280
  function renderPositionLabels(g: CanvasRenderingContext2D,
1185
- dpr: number, hScale: number, positionWidthWithMargin: number, positionWidth: number,
1281
+ dpr: number, positionWidthWithMargin: number, positionWidth: number, positionLabelsHeight: number,
1186
1282
  positions: PositionInfo[], firstVisiblePosIdx: number, lastVisiblePosIdx: number
1187
1283
  ): void {
1188
- for (let jPos = Math.floor(firstVisiblePosIdx); jPos <= Math.floor(lastVisiblePosIdx); jPos++) {
1189
- const pos: PositionInfo = positions[jPos];
1190
- g.resetTransform();
1191
- g.setTransform(
1192
- hScale, 0, 0,
1193
- 1, (jPos - firstVisiblePosIdx) * positionWidthWithMargin * dpr + positionWidth * dpr / 2, 0);
1194
- g.fillText(pos.label, 0, 1);
1284
+ g.save();
1285
+ try {
1286
+ g.textAlign = 'center';
1287
+
1288
+ let maxPosNameWidth: number | null = null;
1289
+ let maxPosNameHeight: number | null = null;
1290
+ for (let jPos = Math.floor(firstVisiblePosIdx); jPos <= Math.floor(lastVisiblePosIdx); jPos++) {
1291
+ const pos = positions[jPos];
1292
+ const tm = g.measureText(pos.name);
1293
+ const textHeight = tm.actualBoundingBoxDescent - tm.actualBoundingBoxAscent;
1294
+ maxPosNameWidth = maxPosNameWidth === null ? tm.width : Math.max(maxPosNameWidth, tm.width);
1295
+ maxPosNameHeight = maxPosNameHeight === null ? textHeight : Math.max(maxPosNameHeight, textHeight);
1296
+ }
1297
+ const hScale = maxPosNameWidth! < (positionWidth * dpr - 2) ? 1 : (positionWidth * dpr - 2) / maxPosNameWidth!;
1298
+
1299
+ for (let jPos = Math.floor(firstVisiblePosIdx); jPos <= Math.floor(lastVisiblePosIdx); jPos++) {
1300
+ const pos: PositionInfo = positions[jPos];
1301
+ const labelCenterX = (jPos - firstVisiblePosIdx) * positionWidthWithMargin * dpr + positionWidth * dpr / 2;
1302
+ const labelTopY = (positionLabelsHeight * dpr - maxPosNameHeight!) / 2;
1303
+ g.setTransform(
1304
+ hScale, 0, 0,
1305
+ 1, labelCenterX, labelTopY);
1306
+ g.measureText(pos.label);
1307
+ g.fillText(pos.label, 0, 0);
1308
+ }
1309
+ } finally {
1310
+ g.restore();
1195
1311
  }
1196
1312
  }
1197
1313