@datagrok/bio 2.25.0 → 2.25.2
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 +14 -0
- package/detectors.js +26 -12
- package/dist/package-test.js +5 -5
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +3 -3
- package/dist/package.js.map +1 -1
- package/package.json +2 -2
- package/scripts/mol-to-helm.py +1279 -0
- package/src/package-api.ts +14 -0
- package/src/package.g.ts +9 -0
- package/src/package.ts +27 -1
- package/src/utils/monomer-lib/library-file-manager/ui.ts +23 -4
- package/src/utils/monomer-lib/monomer-manager/monomer-manager.ts +34 -13
- package/src/utils/seq-helper/seq-handler.ts +15 -6
- package/src/widgets/sequence-scrolling-widget.ts +195 -183
- package/test-console-output-1.log +774 -766
- package/test-record-1.mp4 +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable max-params */
|
|
1
2
|
/* eslint-disable rxjs/no-ignored-subscription */
|
|
2
3
|
/* eslint-disable max-lines */
|
|
3
4
|
/* eslint-disable max-len */
|
|
@@ -11,7 +12,7 @@ import {MonomerPlacer} from '@datagrok-libraries/bio/src/utils/cell-renderer-mon
|
|
|
11
12
|
import {ALPHABET, TAGS as bioTAGS} from '@datagrok-libraries/bio/src/utils/macromolecule';
|
|
12
13
|
import {_package} from '../package';
|
|
13
14
|
import {ISeqHandler} from '@datagrok-libraries/bio/src/utils/macromolecule/seq-handler';
|
|
14
|
-
import * as
|
|
15
|
+
import * as rxjs from 'rxjs';
|
|
15
16
|
import {filter} from 'rxjs/operators';
|
|
16
17
|
import {IMonomerLib, getMonomerLibHelper} from '@datagrok-libraries/bio/src/types/monomer-library';
|
|
17
18
|
import wu from 'wu';
|
|
@@ -322,6 +323,9 @@ class LazyConservationTrack extends ConservationTrack {
|
|
|
322
323
|
// MAIN HANDLER
|
|
323
324
|
// ============================================================================
|
|
324
325
|
|
|
326
|
+
export const MSA_HEADER_INITIALIZED_FLAG = '__msa-scroller-initialized';
|
|
327
|
+
export const MSA_SCROLLER_GRID_SUBSCRIPTION = '__msa-scroller-subscription';
|
|
328
|
+
|
|
325
329
|
export function handleSequenceHeaderRendering() {
|
|
326
330
|
const handleGrid = (grid: DG.Grid) => {
|
|
327
331
|
setTimeout(() => {
|
|
@@ -332,213 +336,221 @@ export function handleSequenceHeaderRendering() {
|
|
|
332
336
|
|
|
333
337
|
const seqCols = df.columns.bySemTypeAll(DG.SEMTYPE.MACROMOLECULE);
|
|
334
338
|
|
|
339
|
+
grid.temp[MSA_SCROLLER_GRID_SUBSCRIPTION]?.unsubscribe();
|
|
340
|
+
grid.temp[MSA_SCROLLER_GRID_SUBSCRIPTION] = DG.debounce(rxjs.merge(df.onColumnsAdded, df.onSemanticTypeDetected), 200).subscribe(() => handleGrid(grid));
|
|
341
|
+
grid.sub(grid.temp[MSA_SCROLLER_GRID_SUBSCRIPTION]);
|
|
342
|
+
|
|
335
343
|
for (const seqCol of seqCols) {
|
|
344
|
+
// first check if the column was already processed
|
|
345
|
+
const gCol = grid.col(seqCol.name);
|
|
346
|
+
if (!gCol) continue;
|
|
347
|
+
|
|
348
|
+
if (gCol.temp[MSA_HEADER_INITIALIZED_FLAG])
|
|
349
|
+
continue;
|
|
350
|
+
gCol.temp[MSA_HEADER_INITIALIZED_FLAG] = true;
|
|
351
|
+
|
|
352
|
+
|
|
336
353
|
let sh: ISeqHandler | null = null;
|
|
337
354
|
|
|
338
355
|
try {
|
|
339
356
|
sh = _package.seqHelper.getSeqHandler(seqCol);
|
|
340
357
|
} catch (_e) {
|
|
358
|
+
console.warn(`Failed to get SeqHandler for column ${seqCol.name}`);
|
|
341
359
|
continue;
|
|
342
360
|
}
|
|
343
|
-
if (!sh)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (!gCol) continue;
|
|
361
|
+
if (!sh)
|
|
362
|
+
continue;
|
|
363
|
+
if (sh.isHelm() || sh.alphabet === ALPHABET.UN)
|
|
364
|
+
continue; // Skip HELM and unknown alphabet, only works for sequences where we know positions of each monomers
|
|
348
365
|
|
|
349
366
|
let positionStatsViewerAddedOnce = !!grid.tableView &&
|
|
350
367
|
Array.from(grid.tableView.viewers).some((v) => v.type === 'Sequence Position Statistics');
|
|
351
368
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
369
|
+
|
|
370
|
+
const ifNan = (a: number, els: number) => (Number.isNaN(a) || a == null ? els : a);
|
|
371
|
+
const getStart = () => ifNan(Math.max(Number.parseInt(seqCol.getTag(bioTAGS.positionShift) ?? '0'), 0), 0) + 1;
|
|
372
|
+
const getCurrent = () => ifNan(Number.parseInt(seqCol.getTag(bioTAGS.selectedPosition) ?? '-2'), -2);
|
|
373
|
+
const getFontSize = () => MonomerPlacer.getFontSettings(seqCol).fontWidth;
|
|
374
|
+
|
|
375
|
+
// Get maximum sequence length. since this scroller is only applicable to Single character monomeric sequences,
|
|
376
|
+
// we do not need to check every single sequence and split it, instead, max length will coorelate with length of the longest string
|
|
377
|
+
let pseudoMaxLenIndex = 0;
|
|
378
|
+
let pseudoMaxLength = 0;
|
|
379
|
+
const cats = seqCol.categories;
|
|
380
|
+
for (let i = 0; i < cats.length; i++) {
|
|
381
|
+
const seq = cats[i];
|
|
382
|
+
if (seq && seq.length > pseudoMaxLength) {
|
|
383
|
+
pseudoMaxLength = seq.length;
|
|
384
|
+
pseudoMaxLenIndex = i;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const seq = cats[pseudoMaxLenIndex];
|
|
388
|
+
const split = sh.splitter(seq);
|
|
389
|
+
const maxSeqLen = split ? split.length : 30;
|
|
390
|
+
|
|
391
|
+
// Do not Skip if sequences are too short, rather, just don't render the tracks by default
|
|
392
|
+
|
|
393
|
+
const STRICT_THRESHOLDS = {
|
|
394
|
+
WITH_TITLE: 58, // BASE + TITLE_HEIGHT(16) + TRACK_GAP(4)
|
|
395
|
+
WITH_WEBLOGO: 107, // WITH_TITLE + DEFAULT_TRACK_HEIGHT(45) + TRACK_GAP(4)
|
|
396
|
+
WITH_BOTH: 156 // WITH_WEBLOGO + DEFAULT_TRACK_HEIGHT(45) + TRACK_GAP(4)
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
let initialHeaderHeight: number;
|
|
400
|
+
if (seqCol.length > 100_000 || maxSeqLen < 50) {
|
|
401
|
+
// Single sequence: just dotted cells
|
|
402
|
+
initialHeaderHeight = STRICT_THRESHOLDS.WITH_TITLE;
|
|
403
|
+
} else {
|
|
404
|
+
if (seqCol.length > 50_000)
|
|
405
|
+
initialHeaderHeight = STRICT_THRESHOLDS.WITH_WEBLOGO;
|
|
406
|
+
else
|
|
407
|
+
initialHeaderHeight = STRICT_THRESHOLDS.WITH_BOTH;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
let webLogoTrackRef: LazyWebLogoTrack | null = null;
|
|
411
|
+
let conservationTrackRef: LazyConservationTrack | null = null;
|
|
412
|
+
const filterChangeSub = DG.debounce(
|
|
413
|
+
rxjs.merge(df.onFilterChanged, df.onDataChanged.pipe(filter((a) => a?.args?.column === seqCol))), 100
|
|
414
|
+
).subscribe(() => {
|
|
415
|
+
MSAViewportManager.clearAllCaches();
|
|
416
|
+
|
|
417
|
+
if (webLogoTrackRef) {
|
|
418
|
+
webLogoTrackRef.resetViewportTracking();
|
|
419
|
+
webLogoTrackRef.forceUpdate();
|
|
420
|
+
}
|
|
421
|
+
if (conservationTrackRef) {
|
|
422
|
+
conservationTrackRef.resetViewportTracking();
|
|
423
|
+
conservationTrackRef.forceUpdate();
|
|
371
424
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
425
|
+
setTimeout(() => {
|
|
426
|
+
if (!grid.isDetached)
|
|
427
|
+
grid.invalidate();
|
|
428
|
+
}, 50);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
grid.sub(filterChangeSub);
|
|
432
|
+
|
|
433
|
+
const initializeHeaders = (monomerLib: IMonomerLib) => {
|
|
434
|
+
const tracks: { id: string, track: MSAHeaderTrack, priority: number }[] = [];
|
|
435
|
+
|
|
436
|
+
// Create lazy tracks only for MSA sequences
|
|
437
|
+
|
|
438
|
+
// OPTIMIZED: Pass seqHandler directly instead of column/splitter
|
|
439
|
+
const conservationTrack = new LazyConservationTrack(
|
|
440
|
+
sh,
|
|
441
|
+
maxSeqLen,
|
|
442
|
+
45, // DEFAULT_TRACK_HEIGHT
|
|
443
|
+
'default',
|
|
444
|
+
'Conservation'
|
|
445
|
+
);
|
|
446
|
+
conservationTrackRef = conservationTrack; // Store reference
|
|
447
|
+
tracks.push({id: 'conservation', track: conservationTrack, priority: 1});
|
|
448
|
+
|
|
449
|
+
// OPTIMIZED: Pass seqHandler directly
|
|
450
|
+
const webLogoTrack = new LazyWebLogoTrack(
|
|
451
|
+
sh,
|
|
452
|
+
maxSeqLen,
|
|
453
|
+
45, // DEFAULT_TRACK_HEIGHT
|
|
454
|
+
'WebLogo'
|
|
455
|
+
);
|
|
456
|
+
webLogoTrackRef = webLogoTrack; // Store reference
|
|
457
|
+
|
|
458
|
+
if (monomerLib) {
|
|
459
|
+
webLogoTrack.setMonomerLib(monomerLib);
|
|
460
|
+
webLogoTrack.setBiotype(sh.defaultBiotype || 'HELM_AA');
|
|
393
461
|
}
|
|
394
462
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
463
|
+
webLogoTrack.setupDefaultTooltip();
|
|
464
|
+
tracks.push({id: 'weblogo', track: webLogoTrack, priority: 2});
|
|
465
|
+
|
|
466
|
+
// Create the scrolling header
|
|
467
|
+
const scroller = new MSAScrollingHeader({
|
|
468
|
+
canvas: grid.overlay,
|
|
469
|
+
headerHeight: initialHeaderHeight,
|
|
470
|
+
totalPositions: maxSeqLen + 1,
|
|
471
|
+
onPositionChange: (scrollerCur, scrollerRange) => {
|
|
472
|
+
setTimeout(() => {
|
|
473
|
+
const start = getStart();
|
|
474
|
+
const cur = getCurrent();
|
|
475
|
+
if (start !== scrollerRange.start)
|
|
476
|
+
seqCol.setTag(bioTAGS.positionShift, (scrollerRange.start - 1).toString());
|
|
477
|
+
|
|
478
|
+
if (cur !== scrollerCur) {
|
|
479
|
+
seqCol.setTag(bioTAGS.selectedPosition, (scrollerCur).toString());
|
|
480
|
+
if (scrollerCur >= 0 && !positionStatsViewerAddedOnce && grid.tableView && wu(grid.dataFrame?.columns.numerical).find((_c) => true)) {
|
|
481
|
+
positionStatsViewerAddedOnce = true;
|
|
482
|
+
const v = grid.tableView.addViewer('Sequence Position Statistics', {sequenceColumnName: seqCol.name});
|
|
483
|
+
grid.tableView.dockManager.dock(v, DG.DOCK_TYPE.DOWN, null, 'Sequence Position Statistics', 0.4);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
},
|
|
488
|
+
onHeaderHeightChange: (_newHeight) => {
|
|
489
|
+
// Update grid header height
|
|
490
|
+
if (grid.isDetached || _newHeight < STRICT_THRESHOLDS.WITH_TITLE) return;
|
|
491
|
+
setTimeout(() => grid.props.colHeaderHeight = _newHeight);
|
|
492
|
+
},
|
|
493
|
+
}, gCol);
|
|
494
|
+
|
|
495
|
+
scroller.setupTooltipHandling();
|
|
496
|
+
|
|
497
|
+
// Add tracks to scroller
|
|
498
|
+
tracks.forEach(({id, track}) => {
|
|
499
|
+
scroller.addTrack(id, track);
|
|
414
500
|
});
|
|
415
501
|
|
|
416
|
-
|
|
502
|
+
scroller.setSelectionData(df, seqCol, sh);
|
|
417
503
|
|
|
418
|
-
|
|
419
|
-
|
|
504
|
+
if (maxSeqLen > 50) {
|
|
505
|
+
grid.props.colHeaderHeight = initialHeaderHeight;
|
|
420
506
|
|
|
421
|
-
//
|
|
507
|
+
// Set column width
|
|
508
|
+
setTimeout(() => {
|
|
509
|
+
if (grid.isDetached) return;
|
|
510
|
+
gCol.width = 400;
|
|
511
|
+
}, 300);
|
|
512
|
+
}
|
|
422
513
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
'default',
|
|
429
|
-
'Conservation'
|
|
430
|
-
);
|
|
431
|
-
conservationTrackRef = conservationTrack; // Store reference
|
|
432
|
-
tracks.push({id: 'conservation', track: conservationTrack, priority: 1});
|
|
433
|
-
|
|
434
|
-
// OPTIMIZED: Pass seqHandler directly
|
|
435
|
-
const webLogoTrack = new LazyWebLogoTrack(
|
|
436
|
-
sh,
|
|
437
|
-
maxSeqLen,
|
|
438
|
-
45, // DEFAULT_TRACK_HEIGHT
|
|
439
|
-
'WebLogo'
|
|
440
|
-
);
|
|
441
|
-
webLogoTrackRef = webLogoTrack; // Store reference
|
|
442
|
-
|
|
443
|
-
if (monomerLib) {
|
|
444
|
-
webLogoTrack.setMonomerLib(monomerLib);
|
|
445
|
-
webLogoTrack.setBiotype(sh.defaultBiotype || 'HELM_AA');
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
webLogoTrack.setupDefaultTooltip();
|
|
449
|
-
tracks.push({id: 'weblogo', track: webLogoTrack, priority: 2});
|
|
450
|
-
|
|
451
|
-
// Create the scrolling header
|
|
452
|
-
const scroller = new MSAScrollingHeader({
|
|
453
|
-
canvas: grid.overlay,
|
|
454
|
-
headerHeight: initialHeaderHeight,
|
|
455
|
-
totalPositions: maxSeqLen + 1,
|
|
456
|
-
onPositionChange: (scrollerCur, scrollerRange) => {
|
|
457
|
-
setTimeout(() => {
|
|
458
|
-
const start = getStart();
|
|
459
|
-
const cur = getCurrent();
|
|
460
|
-
if (start !== scrollerRange.start)
|
|
461
|
-
seqCol.setTag(bioTAGS.positionShift, (scrollerRange.start - 1).toString());
|
|
462
|
-
|
|
463
|
-
if (cur !== scrollerCur) {
|
|
464
|
-
seqCol.setTag(bioTAGS.selectedPosition, (scrollerCur).toString());
|
|
465
|
-
if (scrollerCur >= 0 && !positionStatsViewerAddedOnce && grid.tableView && wu(grid.dataFrame?.columns.numerical).find((_c) => true)) {
|
|
466
|
-
positionStatsViewerAddedOnce = true;
|
|
467
|
-
const v = grid.tableView.addViewer('Sequence Position Statistics', {sequenceColumnName: seqCol.name});
|
|
468
|
-
grid.tableView.dockManager.dock(v, DG.DOCK_TYPE.DOWN, null, 'Sequence Position Statistics', 0.4);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
});
|
|
472
|
-
},
|
|
473
|
-
onHeaderHeightChange: (_newHeight) => {
|
|
474
|
-
// Update grid header height
|
|
475
|
-
if (grid.isDetached || _newHeight < STRICT_THRESHOLDS.WITH_TITLE) return;
|
|
476
|
-
setTimeout(() => grid.props.colHeaderHeight = _newHeight);
|
|
477
|
-
},
|
|
478
|
-
}, gCol);
|
|
514
|
+
// Handle cell rendering for MSA
|
|
515
|
+
grid.sub(grid.onCellRender.subscribe((e) => {
|
|
516
|
+
const cell = e.cell;
|
|
517
|
+
if (!cell || !cell.isColHeader || cell?.gridColumn?.name !== gCol?.name)
|
|
518
|
+
return;
|
|
479
519
|
|
|
480
|
-
|
|
520
|
+
const cellBounds = e.bounds;
|
|
521
|
+
if (!cellBounds) return;
|
|
481
522
|
|
|
482
|
-
//
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
523
|
+
// Set dynamic properties
|
|
524
|
+
scroller.headerHeight = cellBounds.height;
|
|
525
|
+
const font = getFontSize();
|
|
526
|
+
scroller.positionWidth = font + 8; // MSA always has padding
|
|
486
527
|
|
|
487
|
-
|
|
528
|
+
const start = getStart();
|
|
488
529
|
|
|
489
|
-
if (maxSeqLen > 50) {
|
|
490
|
-
grid.props.colHeaderHeight = initialHeaderHeight;
|
|
491
530
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
scroller.draw(
|
|
517
|
-
cellBounds.x,
|
|
518
|
-
cellBounds.y,
|
|
519
|
-
cellBounds.width,
|
|
520
|
-
cellBounds.height,
|
|
521
|
-
getCurrent(),
|
|
522
|
-
start,
|
|
523
|
-
e,
|
|
524
|
-
seqCol.name
|
|
525
|
-
);
|
|
526
|
-
}));
|
|
527
|
-
};
|
|
528
|
-
|
|
529
|
-
// Initialize with monomer library for MSA sequences
|
|
530
|
-
getMonomerLibHelper()
|
|
531
|
-
.then((libHelper) => {
|
|
532
|
-
const monomerLib = libHelper.getMonomerLib();
|
|
533
|
-
initializeHeaders(monomerLib);
|
|
534
|
-
})
|
|
535
|
-
.catch((error) => {
|
|
536
|
-
grok.shell.warning(`Failed to initialize monomer library`);
|
|
537
|
-
console.error('Failed to initialize monomer library:', error);
|
|
538
|
-
});
|
|
539
|
-
} else {
|
|
540
|
-
// For non-MSA sequences, just use standard sequence rendering.
|
|
541
|
-
}
|
|
531
|
+
scroller.draw(
|
|
532
|
+
cellBounds.x,
|
|
533
|
+
cellBounds.y,
|
|
534
|
+
cellBounds.width,
|
|
535
|
+
cellBounds.height,
|
|
536
|
+
getCurrent(),
|
|
537
|
+
start,
|
|
538
|
+
e,
|
|
539
|
+
seqCol.name
|
|
540
|
+
);
|
|
541
|
+
}));
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// Initialize with monomer library for MSA sequences
|
|
545
|
+
getMonomerLibHelper()
|
|
546
|
+
.then((libHelper) => {
|
|
547
|
+
const monomerLib = libHelper.getMonomerLib();
|
|
548
|
+
initializeHeaders(monomerLib);
|
|
549
|
+
})
|
|
550
|
+
.catch((error) => {
|
|
551
|
+
grok.shell.warning(`Failed to initialize monomer library`);
|
|
552
|
+
console.error('Failed to initialize monomer library:', error);
|
|
553
|
+
});
|
|
542
554
|
}
|
|
543
555
|
}, 1000);
|
|
544
556
|
};
|