@datagrok/bio 2.21.11 → 2.22.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 +6 -0
- package/dist/package-test.js +3 -3
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +6 -6
- package/dist/package.js.map +1 -1
- package/package.json +3 -3
- package/src/package.ts +5 -6
- package/src/utils/seq-helper/seq-handler.ts +1 -1
- package/src/viewers/web-logo-viewer.ts +1 -1
- package/src/widgets/composition-analysis-widget.ts +1 -38
- package/src/widgets/sequence-scrolling-widget.ts +492 -89
- package/test-console-output-1.log +315 -327
- package/test-record-1.mp4 +0 -0
- package/src/package.js +0 -529
|
@@ -1,42 +1,356 @@
|
|
|
1
|
+
/* eslint-disable rxjs/no-ignored-subscription */
|
|
2
|
+
/* eslint-disable max-lines */
|
|
1
3
|
/* eslint-disable max-len */
|
|
2
4
|
/* eslint-disable max-lines-per-function */
|
|
3
5
|
import * as grok from 'datagrok-api/grok';
|
|
4
6
|
import * as DG from 'datagrok-api/dg';
|
|
5
7
|
import * as ui from 'datagrok-api/ui';
|
|
6
8
|
|
|
7
|
-
import {MSAScrollingHeader} from '@datagrok-libraries/bio/src/utils/sequence-position-scroller';
|
|
9
|
+
import {ConservationTrack, MSAHeaderTrack, MSAScrollingHeader, WebLogoTrack} from '@datagrok-libraries/bio/src/utils/sequence-position-scroller';
|
|
8
10
|
import {MonomerPlacer} from '@datagrok-libraries/bio/src/utils/cell-renderer-monomer-placer';
|
|
9
11
|
import {ALPHABET, TAGS as bioTAGS} from '@datagrok-libraries/bio/src/utils/macromolecule';
|
|
10
12
|
import {_package} from '../package';
|
|
13
|
+
import {getMonomerLibHelper} from '@datagrok-libraries/bio/src/monomer-works/monomer-utils';
|
|
14
|
+
import {ISeqHandler} from '@datagrok-libraries/bio/src/utils/macromolecule/seq-handler';
|
|
15
|
+
import * as RxJs from 'rxjs';
|
|
16
|
+
import {filter} from 'rxjs/operators';
|
|
17
|
+
import {IMonomerLib} from '@datagrok-libraries/bio/src/types';
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// OPTIMIZED VIEWPORT-AWARE CACHING WITH FORCE UPDATE SUPPORT
|
|
21
|
+
// ============================================================================
|
|
22
|
+
class MSAViewportManager {
|
|
23
|
+
private static conservationCache: DG.LruCache<string, number[]> = new DG.LruCache<string, number[]>(100);
|
|
24
|
+
private static webLogoCache: DG.LruCache<string, Map<number, Map<string, number>>> = new DG.LruCache<string, Map<number, Map<string, number>>>(100);
|
|
25
|
+
|
|
26
|
+
// Track when data was last invalidated for force updates
|
|
27
|
+
private static lastInvalidationTime: number = 0;
|
|
28
|
+
|
|
29
|
+
// Cache chunks of 200 positions at a time
|
|
30
|
+
private static readonly CHUNK_SIZE = 50;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Clear all caches and update invalidation timestamp for force updates
|
|
34
|
+
*/
|
|
35
|
+
static clearAllCaches(): void {
|
|
36
|
+
MSAViewportManager.conservationCache = new DG.LruCache<string, number[]>(100);
|
|
37
|
+
MSAViewportManager.webLogoCache = new DG.LruCache<string, Map<number, Map<string, number>>>(100);
|
|
38
|
+
MSAViewportManager.lastInvalidationTime = Date.now();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get the last invalidation time (used by tracks to detect forced updates)
|
|
43
|
+
*/
|
|
44
|
+
static getLastInvalidationTime(): number {
|
|
45
|
+
return MSAViewportManager.lastInvalidationTime;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private static getChunkCacheKey(column: DG.Column, chunkStart: number, chunkEnd: number): string {
|
|
49
|
+
const dataFrame = column.dataFrame;
|
|
50
|
+
// Use framework's built-in versioning instead of complex hash
|
|
51
|
+
return `${dataFrame.id}_${column.name}_f${dataFrame.filter.version}_${chunkStart}_${chunkEnd}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private static getConservationChunk(seqHandler: ISeqHandler, start: number, end: number, cacheKey: string): number[] {
|
|
55
|
+
return MSAViewportManager.conservationCache.getOrCreate(cacheKey, () => {
|
|
56
|
+
// conservation track is only shown together with weblogo track, so we can reuse weblogo for this caluclation
|
|
57
|
+
const webLogoChunk = MSAViewportManager.getWebLogoChunk(seqHandler, start, end, cacheKey);
|
|
58
|
+
const chunkSize = end - start;
|
|
59
|
+
const scores = new Array(chunkSize).fill(0);
|
|
60
|
+
for (let i = start; i < end; i++) {
|
|
61
|
+
const entries = webLogoChunk.get(i);
|
|
62
|
+
if (!entries || entries.size === 0)
|
|
63
|
+
continue;
|
|
64
|
+
let totalResidues = 0;
|
|
65
|
+
let mostCommonCount = 0;
|
|
66
|
+
const gapS = seqHandler.defaultGapOriginal;
|
|
67
|
+
// Single pass to find both total and max
|
|
68
|
+
for (const [mon, count] of entries.entries()) {
|
|
69
|
+
if (!mon || mon === gapS) continue; // Skip gaps
|
|
70
|
+
totalResidues += count;
|
|
71
|
+
if (count > mostCommonCount)
|
|
72
|
+
mostCommonCount = count;
|
|
73
|
+
}
|
|
74
|
+
// Calculate conservation score
|
|
75
|
+
scores[i - start] = totalResidues > 0 ? mostCommonCount / totalResidues : 0;
|
|
76
|
+
}
|
|
77
|
+
return scores;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private static getWebLogoChunk(seqHandler: ISeqHandler, start: number, end: number, cacheKey: string): Map<number, Map<string, number>> {
|
|
82
|
+
return MSAViewportManager.webLogoCache.getOrCreate(cacheKey, () => {
|
|
83
|
+
const dataFrame = seqHandler.column.dataFrame;
|
|
84
|
+
const webLogoData: Map<number, Map<string, number>> = new Map();
|
|
85
|
+
// OPTIMIZED: Use filter iterator
|
|
86
|
+
const visibleCount = dataFrame.filter.trueCount;
|
|
87
|
+
if (visibleCount <= 1)
|
|
88
|
+
return webLogoData;
|
|
89
|
+
|
|
90
|
+
// pre-alocate residue counts map
|
|
91
|
+
for (let i = start; i < end; i++)
|
|
92
|
+
webLogoData.set(i, new Map());
|
|
93
|
+
|
|
94
|
+
const filter = dataFrame.filter;
|
|
95
|
+
|
|
96
|
+
const oneOverCount = 1 / visibleCount; // Pre-calculate for performance
|
|
97
|
+
|
|
98
|
+
for (let i = -1; (i = filter.findNext(i, true)) !== -1;) {
|
|
99
|
+
// OPTIMIZED: Use seqHandler.getSplitted which has internal caching
|
|
100
|
+
const seqChunk = seqHandler.getSplitted(i).getOriginalRegion(start, end);
|
|
101
|
+
if (seqChunk.length === 0) continue;
|
|
102
|
+
for (let pos = 0; pos < seqChunk.length; pos++) {
|
|
103
|
+
const residue = seqChunk[pos];
|
|
104
|
+
// in weblogo, do not skip gaps, they are important
|
|
105
|
+
const residueMap = webLogoData.get(start + pos)!;
|
|
106
|
+
// add oneOverCount here to avoid division later
|
|
107
|
+
residueMap.set(residue, (residueMap.get(residue) || 0) + oneOverCount);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return webLogoData;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
static getConservationForViewport(seqHandler: ISeqHandler, viewportStart: number, viewportEnd: number, maxLength: number): number[] {
|
|
115
|
+
// Create a full-sized array filled with zeros
|
|
116
|
+
const result: number[] = new Array(maxLength).fill(0);
|
|
117
|
+
|
|
118
|
+
// Calculate which chunks we need
|
|
119
|
+
const startChunk = Math.floor(viewportStart / MSAViewportManager.CHUNK_SIZE) * MSAViewportManager.CHUNK_SIZE;
|
|
120
|
+
const endChunk = Math.ceil(viewportEnd / MSAViewportManager.CHUNK_SIZE) * MSAViewportManager.CHUNK_SIZE;
|
|
121
|
+
|
|
122
|
+
// Get all needed chunks and place them in the correct positions
|
|
123
|
+
for (let chunkStart = startChunk; chunkStart < endChunk; chunkStart += MSAViewportManager.CHUNK_SIZE) {
|
|
124
|
+
const chunkEnd = Math.min(chunkStart + MSAViewportManager.CHUNK_SIZE, maxLength);
|
|
125
|
+
const cacheKey = MSAViewportManager.getChunkCacheKey(seqHandler.column, chunkStart, chunkEnd);
|
|
126
|
+
const chunkData = MSAViewportManager.getConservationChunk(seqHandler, chunkStart, chunkEnd, cacheKey);
|
|
127
|
+
|
|
128
|
+
// Copy chunk data to the correct positions in the result array
|
|
129
|
+
for (let i = 0; i < chunkData.length && (chunkStart + i) < maxLength; i++)
|
|
130
|
+
result[chunkStart + i] = chunkData[i];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
static getWebLogoForViewport(seqHandler: ISeqHandler, viewportStart: number, viewportEnd: number, maxLength: number): Map<number, Map<string, number>> {
|
|
137
|
+
const result: Map<number, Map<string, number>> = new Map();
|
|
138
|
+
|
|
139
|
+
// Calculate which chunks we need
|
|
140
|
+
const startChunk = Math.floor(viewportStart / MSAViewportManager.CHUNK_SIZE) * MSAViewportManager.CHUNK_SIZE;
|
|
141
|
+
const endChunk = Math.ceil(viewportEnd / MSAViewportManager.CHUNK_SIZE) * MSAViewportManager.CHUNK_SIZE;
|
|
142
|
+
|
|
143
|
+
// Get all needed chunks and place them in the correct positions
|
|
144
|
+
for (let chunkStart = startChunk; chunkStart < endChunk; chunkStart += MSAViewportManager.CHUNK_SIZE) {
|
|
145
|
+
const chunkEnd = Math.min(chunkStart + MSAViewportManager.CHUNK_SIZE, maxLength);
|
|
146
|
+
const cacheKey = MSAViewportManager.getChunkCacheKey(seqHandler.column, chunkStart, chunkEnd);
|
|
147
|
+
const chunkData = MSAViewportManager.getWebLogoChunk(seqHandler, chunkStart, chunkEnd, cacheKey);
|
|
148
|
+
|
|
149
|
+
// Copy chunk data to the result map (positions are already correct)
|
|
150
|
+
for (const [pos, data] of chunkData.entries()) {
|
|
151
|
+
if (pos < maxLength)
|
|
152
|
+
result.set(pos, data);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ============================================================================
|
|
161
|
+
// SMART TRACKS
|
|
162
|
+
// ============================================================================
|
|
163
|
+
|
|
164
|
+
class LazyWebLogoTrack extends WebLogoTrack {
|
|
165
|
+
private seqHandler: ISeqHandler;
|
|
166
|
+
private maxLength: number;
|
|
167
|
+
private lastViewportStart: number = -1;
|
|
168
|
+
private lastViewportEnd: number = -1;
|
|
169
|
+
private lastInvalidationTime: number = 0;
|
|
170
|
+
private forceNextUpdate: boolean = false;
|
|
171
|
+
|
|
172
|
+
constructor(
|
|
173
|
+
seqHandler: ISeqHandler,
|
|
174
|
+
maxLength: number,
|
|
175
|
+
height: number = 45,
|
|
176
|
+
title: string = 'WebLogo'
|
|
177
|
+
) {
|
|
178
|
+
super(new Map(), height, '', title);
|
|
179
|
+
this.seqHandler = seqHandler;
|
|
180
|
+
this.maxLength = maxLength;
|
|
181
|
+
|
|
182
|
+
this.visible = seqHandler.column.dataFrame.filter.trueCount > 1;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
public forceUpdate(): void {
|
|
186
|
+
this.forceNextUpdate = true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
public resetViewportTracking(): void {
|
|
190
|
+
this.lastViewportStart = -1;
|
|
191
|
+
this.lastViewportEnd = -1;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private updateForViewport(viewportStart: number, viewportEnd: number): void {
|
|
195
|
+
const currentInvalidationTime = MSAViewportManager.getLastInvalidationTime();
|
|
196
|
+
const cacheWasInvalidated = currentInvalidationTime > this.lastInvalidationTime;
|
|
197
|
+
|
|
198
|
+
const viewportChanged = (
|
|
199
|
+
Math.abs(this.lastViewportStart - viewportStart) >= 10 ||
|
|
200
|
+
Math.abs(this.lastViewportEnd - viewportEnd) >= 10
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Only update if viewport changed significantly OR cache was invalidated OR force update is set
|
|
204
|
+
if (!viewportChanged && !cacheWasInvalidated && !this.forceNextUpdate)
|
|
205
|
+
return;
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
// Reset flags and update tracking
|
|
209
|
+
this.forceNextUpdate = false;
|
|
210
|
+
this.lastViewportStart = viewportStart;
|
|
211
|
+
this.lastViewportEnd = viewportEnd;
|
|
212
|
+
this.lastInvalidationTime = currentInvalidationTime;
|
|
213
|
+
|
|
214
|
+
// Add buffer to reduce cache misses on small scrolls
|
|
215
|
+
const bufferedStart = Math.max(0, viewportStart - 20);
|
|
216
|
+
const bufferedEnd = Math.min(this.maxLength, viewportEnd + 20);
|
|
217
|
+
|
|
218
|
+
const webLogoData = MSAViewportManager.getWebLogoForViewport(
|
|
219
|
+
this.seqHandler,
|
|
220
|
+
bufferedStart,
|
|
221
|
+
bufferedEnd,
|
|
222
|
+
this.maxLength
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
this.updateData(webLogoData);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
draw(x: number, y: number, width: number, height: number, windowStart: number,
|
|
229
|
+
positionWidth: number, totalPositions: number, currentPosition: number): void {
|
|
230
|
+
// Calculate what positions are actually visible
|
|
231
|
+
const visiblePositions = Math.ceil(width / positionWidth) + 2;
|
|
232
|
+
const viewportEnd = Math.min(windowStart + visiblePositions, totalPositions);
|
|
233
|
+
|
|
234
|
+
// Update data for just the visible range
|
|
235
|
+
this.updateForViewport(windowStart - 1, viewportEnd - 1); // Convert to 0-based
|
|
236
|
+
|
|
237
|
+
// Call parent draw method
|
|
238
|
+
super.draw(x, y, width, height, windowStart, positionWidth, totalPositions, currentPosition);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
class LazyConservationTrack extends ConservationTrack {
|
|
243
|
+
private seqHandler: ISeqHandler;
|
|
244
|
+
private maxLength: number;
|
|
245
|
+
private lastViewportStart: number = -1;
|
|
246
|
+
private lastViewportEnd: number = -1;
|
|
247
|
+
private lastInvalidationTime: number = 0;
|
|
248
|
+
private forceNextUpdate: boolean = false;
|
|
249
|
+
|
|
250
|
+
constructor(
|
|
251
|
+
seqHandler: ISeqHandler,
|
|
252
|
+
maxLength: number,
|
|
253
|
+
height: number = 45,
|
|
254
|
+
colorScheme: 'default' | 'rainbow' | 'heatmap' = 'default',
|
|
255
|
+
title: string = 'Conservation'
|
|
256
|
+
) {
|
|
257
|
+
super([], height, colorScheme, title);
|
|
258
|
+
this.seqHandler = seqHandler;
|
|
259
|
+
this.maxLength = maxLength;
|
|
260
|
+
|
|
261
|
+
// OPTIMIZED: Use dataFrame.filter.trueCount directly
|
|
262
|
+
this.visible = seqHandler.column.dataFrame.filter.trueCount > 1;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
public forceUpdate(): void {
|
|
266
|
+
this.forceNextUpdate = true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
public resetViewportTracking(): void {
|
|
270
|
+
this.lastViewportStart = -1;
|
|
271
|
+
this.lastViewportEnd = -1;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private updateForViewport(viewportStart: number, viewportEnd: number): void {
|
|
275
|
+
const currentInvalidationTime = MSAViewportManager.getLastInvalidationTime();
|
|
276
|
+
const cacheWasInvalidated = currentInvalidationTime > this.lastInvalidationTime;
|
|
277
|
+
|
|
278
|
+
const viewportChanged = (
|
|
279
|
+
Math.abs(this.lastViewportStart - viewportStart) >= 10 ||
|
|
280
|
+
Math.abs(this.lastViewportEnd - viewportEnd) >= 10
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
// Only update if viewport changed significantly OR cache was invalidated OR force update is set
|
|
284
|
+
if (!viewportChanged && !cacheWasInvalidated && !this.forceNextUpdate)
|
|
285
|
+
return;
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
// Reset flags and update tracking
|
|
289
|
+
this.forceNextUpdate = false;
|
|
290
|
+
this.lastViewportStart = viewportStart;
|
|
291
|
+
this.lastViewportEnd = viewportEnd;
|
|
292
|
+
this.lastInvalidationTime = currentInvalidationTime;
|
|
293
|
+
|
|
294
|
+
// Add buffer to reduce cache misses on small scrolls
|
|
295
|
+
const bufferedStart = Math.max(0, viewportStart - 20);
|
|
296
|
+
const bufferedEnd = Math.min(this.maxLength, viewportEnd + 20);
|
|
297
|
+
|
|
298
|
+
const conservationScores = MSAViewportManager.getConservationForViewport(
|
|
299
|
+
this.seqHandler,
|
|
300
|
+
bufferedStart,
|
|
301
|
+
bufferedEnd,
|
|
302
|
+
this.maxLength
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
this.updateData(conservationScores);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
draw(x: number, y: number, width: number, height: number, windowStart: number,
|
|
309
|
+
positionWidth: number, totalPositions: number, currentPosition: number): void {
|
|
310
|
+
// Calculate what positions are actually visible
|
|
311
|
+
const visiblePositions = Math.ceil(width / positionWidth) + 2;
|
|
312
|
+
const viewportEnd = Math.min(windowStart + visiblePositions, totalPositions);
|
|
313
|
+
|
|
314
|
+
this.updateForViewport(windowStart - 1, viewportEnd - 1); // Convert to 0-based
|
|
315
|
+
|
|
316
|
+
// Call parent draw method
|
|
317
|
+
super.draw(x, y, width, height, windowStart, positionWidth, totalPositions, currentPosition);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ============================================================================
|
|
322
|
+
// MAIN HANDLER
|
|
323
|
+
// ============================================================================
|
|
11
324
|
|
|
12
325
|
export function handleSequenceHeaderRendering() {
|
|
13
326
|
const handleGrid = (grid: DG.Grid) => {
|
|
14
327
|
setTimeout(() => {
|
|
15
|
-
if (grid.isDetached)
|
|
16
|
-
|
|
328
|
+
if (grid.isDetached) return;
|
|
329
|
+
|
|
17
330
|
const df = grid.dataFrame;
|
|
18
|
-
if (!df)
|
|
19
|
-
|
|
331
|
+
if (!df) return;
|
|
332
|
+
|
|
20
333
|
const seqCols = df.columns.bySemTypeAll(DG.SEMTYPE.MACROMOLECULE);
|
|
334
|
+
|
|
21
335
|
for (const seqCol of seqCols) {
|
|
22
|
-
// TODO: Extend this to non-canonical monomers and non msa
|
|
23
336
|
const sh = _package.seqHelper.getSeqHandler(seqCol);
|
|
24
|
-
if (!sh)
|
|
25
|
-
|
|
26
|
-
if (sh.isHelm() || sh.alphabet === ALPHABET.UN)
|
|
27
|
-
continue;
|
|
337
|
+
if (!sh) continue;
|
|
338
|
+
if (sh.isHelm() || sh.alphabet === ALPHABET.UN) continue;
|
|
28
339
|
|
|
29
340
|
const gCol = grid.col(seqCol.name);
|
|
30
|
-
if (!gCol)
|
|
31
|
-
|
|
341
|
+
if (!gCol) continue;
|
|
342
|
+
|
|
343
|
+
let positionStatsViewerAddedOnce = !!grid.tableView &&
|
|
344
|
+
Array.from(grid.tableView.viewers).some((v) => v.type === 'Sequence Position Statistics');
|
|
32
345
|
|
|
33
|
-
let positionStatsViewerAddedOnce = !!grid.tableView && Array.from(grid.tableView.viewers).some((v) => v.type === 'Sequence Position Statistics');
|
|
34
346
|
const isMSA = sh.isMsa();
|
|
35
347
|
const ifNan = (a: number, els: number) => (Number.isNaN(a) ? els : a);
|
|
36
348
|
const getStart = () => ifNan(Math.max(Number.parseInt(seqCol.getTag(bioTAGS.positionShift) ?? '0'), 0), 0) + 1;
|
|
37
349
|
const getCurrent = () => ifNan(Number.parseInt(seqCol.getTag(bioTAGS.selectedPosition) ?? '-2'), -2);
|
|
38
350
|
const getFontSize = () => MonomerPlacer.getFontSettings(seqCol).fontWidth;
|
|
39
|
-
|
|
351
|
+
|
|
352
|
+
// Get maximum sequence length. since this scroller is only applicable to Single character monomeric sequences,
|
|
353
|
+
// we do not need to check every single sequence and split it, instead, max length will coorelate with length of the longest string
|
|
40
354
|
let pseudoMaxLenIndex = 0;
|
|
41
355
|
let pseudoMaxLength = 0;
|
|
42
356
|
const cats = seqCol.categories;
|
|
@@ -51,88 +365,178 @@ export function handleSequenceHeaderRendering() {
|
|
|
51
365
|
const split = sh.splitter(seq);
|
|
52
366
|
const maxSeqLen = split ? split.length : 30;
|
|
53
367
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
368
|
+
// Do not Skip if sequences are too short, rather, just don't render the tracks by default
|
|
369
|
+
|
|
370
|
+
const STRICT_THRESHOLDS = {
|
|
371
|
+
WITH_TITLE: 58, // BASE + TITLE_HEIGHT(16) + TRACK_GAP(4)
|
|
372
|
+
WITH_WEBLOGO: 107, // WITH_TITLE + DEFAULT_TRACK_HEIGHT(45) + TRACK_GAP(4)
|
|
373
|
+
WITH_BOTH: 156 // WITH_WEBLOGO + DEFAULT_TRACK_HEIGHT(45) + TRACK_GAP(4)
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
let initialHeaderHeight: number;
|
|
377
|
+
if (seqCol.length > 100_000 || maxSeqLen < 50) {
|
|
378
|
+
// Single sequence: just dotted cells
|
|
379
|
+
initialHeaderHeight = STRICT_THRESHOLDS.WITH_TITLE;
|
|
380
|
+
} else {
|
|
381
|
+
if (seqCol.length > 50_000)
|
|
382
|
+
initialHeaderHeight = STRICT_THRESHOLDS.WITH_WEBLOGO;
|
|
383
|
+
else
|
|
384
|
+
initialHeaderHeight = STRICT_THRESHOLDS.WITH_BOTH;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
let webLogoTrackRef: LazyWebLogoTrack | null = null;
|
|
388
|
+
let conservationTrackRef: LazyConservationTrack | null = null;
|
|
389
|
+
const filterChangeSub = DG.debounce(
|
|
390
|
+
RxJs.merge(df.onFilterChanged, df.onDataChanged.pipe(filter((a) => a?.args?.column === seqCol))), 100
|
|
391
|
+
).subscribe(() => {
|
|
392
|
+
MSAViewportManager.clearAllCaches();
|
|
393
|
+
|
|
394
|
+
if (webLogoTrackRef) {
|
|
395
|
+
webLogoTrackRef.resetViewportTracking();
|
|
396
|
+
webLogoTrackRef.forceUpdate();
|
|
397
|
+
}
|
|
398
|
+
if (conservationTrackRef) {
|
|
399
|
+
conservationTrackRef.resetViewportTracking();
|
|
400
|
+
conservationTrackRef.forceUpdate();
|
|
401
|
+
}
|
|
402
|
+
setTimeout(() => {
|
|
403
|
+
if (!grid.isDetached)
|
|
404
|
+
grid.invalidate();
|
|
405
|
+
}, 50);
|
|
75
406
|
});
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
407
|
+
|
|
408
|
+
grid.sub(filterChangeSub);
|
|
409
|
+
|
|
410
|
+
const initializeHeaders = (monomerLib: IMonomerLib) => {
|
|
411
|
+
const tracks: { id: string, track: MSAHeaderTrack, priority: number }[] = [];
|
|
412
|
+
|
|
413
|
+
// Create lazy tracks only if we have multiple sequences
|
|
414
|
+
|
|
415
|
+
// OPTIMIZED: Pass seqHandler directly instead of column/splitter
|
|
416
|
+
const conservationTrack = new LazyConservationTrack(
|
|
417
|
+
sh,
|
|
418
|
+
maxSeqLen,
|
|
419
|
+
45, // DEFAULT_TRACK_HEIGHT
|
|
420
|
+
'default',
|
|
421
|
+
'Conservation'
|
|
422
|
+
);
|
|
423
|
+
conservationTrackRef = conservationTrack; // Store reference
|
|
424
|
+
tracks.push({id: 'conservation', track: conservationTrack, priority: 1});
|
|
425
|
+
|
|
426
|
+
// OPTIMIZED: Pass seqHandler directly
|
|
427
|
+
const webLogoTrack = new LazyWebLogoTrack(
|
|
428
|
+
sh,
|
|
429
|
+
maxSeqLen,
|
|
430
|
+
45, // DEFAULT_TRACK_HEIGHT
|
|
431
|
+
'WebLogo'
|
|
432
|
+
);
|
|
433
|
+
webLogoTrackRef = webLogoTrack; // Store reference
|
|
434
|
+
|
|
435
|
+
if (monomerLib) {
|
|
436
|
+
webLogoTrack.setMonomerLib(monomerLib);
|
|
437
|
+
webLogoTrack.setBiotype(sh.defaultBiotype || 'HELM_AA');
|
|
89
438
|
}
|
|
90
439
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
440
|
+
webLogoTrack.setupDefaultTooltip();
|
|
441
|
+
tracks.push({id: 'weblogo', track: webLogoTrack, priority: 2});
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
// Create the scrolling header
|
|
445
|
+
const scroller = new MSAScrollingHeader({
|
|
446
|
+
canvas: grid.overlay,
|
|
447
|
+
headerHeight: initialHeaderHeight,
|
|
448
|
+
totalPositions: maxSeqLen + 1,
|
|
449
|
+
onPositionChange: (scrollerCur, scrollerRange) => {
|
|
450
|
+
setTimeout(() => {
|
|
451
|
+
const start = getStart();
|
|
452
|
+
const cur = getCurrent();
|
|
453
|
+
if (start !== scrollerRange.start)
|
|
454
|
+
seqCol.setTag(bioTAGS.positionShift, (scrollerRange.start - 1).toString());
|
|
455
|
+
|
|
456
|
+
if (cur !== scrollerCur) {
|
|
457
|
+
seqCol.setTag(bioTAGS.selectedPosition, (scrollerCur).toString());
|
|
458
|
+
if (scrollerCur >= 0 && !positionStatsViewerAddedOnce && grid.tableView) {
|
|
459
|
+
positionStatsViewerAddedOnce = true;
|
|
460
|
+
const v = grid.tableView.addViewer('Sequence Position Statistics', {sequenceColumnName: seqCol.name});
|
|
461
|
+
grid.tableView.dockManager.dock(v, DG.DOCK_TYPE.DOWN, null, 'Sequence Position Statistics', 0.4);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
},
|
|
466
|
+
onHeaderHeightChange: (_newHeight) => {
|
|
467
|
+
// Update grid header height
|
|
468
|
+
if (grid.isDetached || _newHeight < STRICT_THRESHOLDS.WITH_TITLE) return;
|
|
469
|
+
setTimeout(() => grid.props.colHeaderHeight = _newHeight);
|
|
470
|
+
},
|
|
471
|
+
});
|
|
97
472
|
|
|
98
|
-
|
|
99
|
-
|
|
473
|
+
scroller.setupTooltipHandling();
|
|
474
|
+
|
|
475
|
+
// Add tracks to scroller
|
|
476
|
+
tracks.forEach(({id, track}) => {
|
|
477
|
+
scroller.addTrack(id, track);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
scroller.setSelectionData(df, seqCol, sh);
|
|
481
|
+
|
|
482
|
+
if (maxSeqLen > 50) {
|
|
483
|
+
grid.props.colHeaderHeight = initialHeaderHeight;
|
|
484
|
+
|
|
485
|
+
// Set column width
|
|
486
|
+
setTimeout(() => {
|
|
487
|
+
if (grid.isDetached) return;
|
|
488
|
+
gCol.width = 400;
|
|
489
|
+
}, 300);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Handle cell rendering
|
|
493
|
+
grid.sub(grid.onCellRender.subscribe((e) => {
|
|
494
|
+
const cell = e.cell;
|
|
495
|
+
if (!cell || !cell.isColHeader || cell?.gridColumn?.name !== gCol?.name)
|
|
100
496
|
return;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
const cellBounds = e.bounds;
|
|
500
|
+
if (!cellBounds) return;
|
|
501
|
+
|
|
502
|
+
// Set dynamic properties
|
|
503
|
+
scroller.headerHeight = cellBounds.height;
|
|
504
|
+
const font = getFontSize();
|
|
505
|
+
scroller.positionWidth = font + (isMSA ? 8 : 0);
|
|
506
|
+
|
|
507
|
+
const start = getStart();
|
|
508
|
+
const startPadding = isMSA ? 0 : 4;
|
|
509
|
+
|
|
510
|
+
scroller.draw(
|
|
511
|
+
cellBounds.x + startPadding,
|
|
512
|
+
cellBounds.y,
|
|
513
|
+
cellBounds.width - startPadding,
|
|
514
|
+
cellBounds.height,
|
|
515
|
+
getCurrent(),
|
|
516
|
+
start,
|
|
517
|
+
e,
|
|
518
|
+
seqCol.name
|
|
519
|
+
);
|
|
520
|
+
}));
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// Initialize with monomer library
|
|
524
|
+
getMonomerLibHelper()
|
|
525
|
+
.then((libHelper) => {
|
|
526
|
+
const monomerLib = libHelper.getMonomerLib();
|
|
527
|
+
initializeHeaders(monomerLib);
|
|
528
|
+
})
|
|
529
|
+
.catch((error) => {
|
|
530
|
+
grok.shell.warning(`Failed to initialize monomer library`);
|
|
531
|
+
//initializeHeaders();
|
|
532
|
+
console.error('Failed to initialize monomer library:', error);
|
|
533
|
+
});
|
|
129
534
|
}
|
|
130
535
|
}, 1000);
|
|
131
536
|
};
|
|
132
|
-
|
|
537
|
+
|
|
133
538
|
const _ = grok.events.onViewerAdded.subscribe((e) => {
|
|
134
|
-
if (!e.args || !(e.args.viewer instanceof DG.Grid))
|
|
135
|
-
return;
|
|
539
|
+
if (!e.args || !(e.args.viewer instanceof DG.Grid)) return;
|
|
136
540
|
const grid = e.args.viewer as DG.Grid;
|
|
137
541
|
handleGrid(grid);
|
|
138
542
|
});
|
|
@@ -140,7 +544,6 @@ export function handleSequenceHeaderRendering() {
|
|
|
140
544
|
const openTables = grok.shell.tableViews;
|
|
141
545
|
for (const tv of openTables) {
|
|
142
546
|
const grid = tv?.grid;
|
|
143
|
-
if (grid)
|
|
144
|
-
handleGrid(grid);
|
|
547
|
+
if (grid) handleGrid(grid);
|
|
145
548
|
}
|
|
146
549
|
}
|