@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.
- package/CHANGELOG.md +4 -2
- package/dist/package-test.js +1 -1
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/package.json +2 -2
- package/src/apps/web-logo-app.ts +7 -3
- package/src/package.ts +29 -10
- package/src/tests/WebLogo-positions-test.ts +3 -3
- package/src/utils/agg.ts +29 -0
- package/src/utils/poly-tool/const.ts +16 -0
- package/src/utils/{enumerator-tools.ts → poly-tool/enumerator-tools.ts} +36 -54
- package/src/utils/poly-tool/types.ts +14 -0
- package/src/utils/poly-tool/utils.ts +20 -0
- package/src/viewers/web-logo-viewer.ts +235 -119
- package/src/widgets/composition-analysis-widget.ts +35 -23
- package/webpack.config.js +1 -1
|
@@ -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
|
|
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(
|
|
52
|
-
this.
|
|
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
|
-
|
|
70
|
-
|
|
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?: {
|
|
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?.
|
|
87
|
-
if (options?.
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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.
|
|
155
|
+
this.sumPlotValueForHeight = 0;
|
|
133
156
|
if (heightMode === PositionHeight.Entropy) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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 [
|
|
140
|
-
const pn =
|
|
141
|
-
this.
|
|
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,
|
|
175
|
+
positionWidthWithMargin: number, positionWidth: number, dpr: number, positionLabelsHeight: number
|
|
151
176
|
): void {
|
|
152
|
-
const maxHeight = (heightMode
|
|
153
|
-
(absoluteMaxHeight * (alphabetSizeLog - (this.
|
|
177
|
+
const maxHeight = (heightMode === PositionHeight.Entropy) ?
|
|
178
|
+
(absoluteMaxHeight * (alphabetSizeLog - (this.sumPlotValueForHeight)) / alphabetSizeLog) :
|
|
154
179
|
absoluteMaxHeight;
|
|
155
|
-
let y: number =
|
|
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].
|
|
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
|
|
169
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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): [
|
|
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
|
|
930
|
+
const pi: PositionInfo = this.positions[jPos];
|
|
868
931
|
|
|
869
|
-
if (
|
|
870
|
-
return [
|
|
932
|
+
if (!pi)
|
|
933
|
+
return [null, null, null];
|
|
871
934
|
|
|
872
|
-
const monomer: string | undefined =
|
|
935
|
+
const monomer: string | undefined = pi.getMonomerAt(calculatedX, p.y);
|
|
873
936
|
if (monomer === undefined)
|
|
874
|
-
return [
|
|
937
|
+
return [pi, null, null];
|
|
875
938
|
|
|
876
|
-
return [
|
|
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).
|
|
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
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
|
946
|
-
|
|
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
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
this.positions[jPos].
|
|
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
|
-
|
|
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
|
-
|
|
967
|
-
if (
|
|
968
|
-
|
|
969
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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,
|
|
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 [
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
const
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
const
|
|
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(`${
|
|
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 [
|
|
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
|
|
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,
|
|
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,
|
|
1281
|
+
dpr: number, positionWidthWithMargin: number, positionWidth: number, positionLabelsHeight: number,
|
|
1186
1282
|
positions: PositionInfo[], firstVisiblePosIdx: number, lastVisiblePosIdx: number
|
|
1187
1283
|
): void {
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
g.
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
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
|
|