@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.
@@ -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 RxJs from 'rxjs';
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) continue;
344
- if (sh.isHelm() || sh.alphabet === ALPHABET.UN) continue;
345
-
346
- const gCol = grid.col(seqCol.name);
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
- const isMSA = sh.isMsa();
353
-
354
- if (isMSA) {
355
- const ifNan = (a: number, els: number) => (Number.isNaN(a) ? els : a);
356
- const getStart = () => ifNan(Math.max(Number.parseInt(seqCol.getTag(bioTAGS.positionShift) ?? '0'), 0), 0) + 1;
357
- const getCurrent = () => ifNan(Number.parseInt(seqCol.getTag(bioTAGS.selectedPosition) ?? '-2'), -2);
358
- const getFontSize = () => MonomerPlacer.getFontSettings(seqCol).fontWidth;
359
-
360
- // Get maximum sequence length. since this scroller is only applicable to Single character monomeric sequences,
361
- // we do not need to check every single sequence and split it, instead, max length will coorelate with length of the longest string
362
- let pseudoMaxLenIndex = 0;
363
- let pseudoMaxLength = 0;
364
- const cats = seqCol.categories;
365
- for (let i = 0; i < cats.length; i++) {
366
- const seq = cats[i];
367
- if (seq && seq.length > pseudoMaxLength) {
368
- pseudoMaxLength = seq.length;
369
- pseudoMaxLenIndex = i;
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
- const seq = cats[pseudoMaxLenIndex];
373
- const split = sh.splitter(seq);
374
- const maxSeqLen = split ? split.length : 30;
375
-
376
- // Do not Skip if sequences are too short, rather, just don't render the tracks by default
377
-
378
- const STRICT_THRESHOLDS = {
379
- WITH_TITLE: 58, // BASE + TITLE_HEIGHT(16) + TRACK_GAP(4)
380
- WITH_WEBLOGO: 107, // WITH_TITLE + DEFAULT_TRACK_HEIGHT(45) + TRACK_GAP(4)
381
- WITH_BOTH: 156 // WITH_WEBLOGO + DEFAULT_TRACK_HEIGHT(45) + TRACK_GAP(4)
382
- };
383
-
384
- let initialHeaderHeight: number;
385
- if (seqCol.length > 100_000 || maxSeqLen < 50) {
386
- // Single sequence: just dotted cells
387
- initialHeaderHeight = STRICT_THRESHOLDS.WITH_TITLE;
388
- } else {
389
- if (seqCol.length > 50_000)
390
- initialHeaderHeight = STRICT_THRESHOLDS.WITH_WEBLOGO;
391
- else
392
- initialHeaderHeight = STRICT_THRESHOLDS.WITH_BOTH;
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
- let webLogoTrackRef: LazyWebLogoTrack | null = null;
396
- let conservationTrackRef: LazyConservationTrack | null = null;
397
- const filterChangeSub = DG.debounce(
398
- RxJs.merge(df.onFilterChanged, df.onDataChanged.pipe(filter((a) => a?.args?.column === seqCol))), 100
399
- ).subscribe(() => {
400
- MSAViewportManager.clearAllCaches();
401
-
402
- if (webLogoTrackRef) {
403
- webLogoTrackRef.resetViewportTracking();
404
- webLogoTrackRef.forceUpdate();
405
- }
406
- if (conservationTrackRef) {
407
- conservationTrackRef.resetViewportTracking();
408
- conservationTrackRef.forceUpdate();
409
- }
410
- setTimeout(() => {
411
- if (!grid.isDetached)
412
- grid.invalidate();
413
- }, 50);
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
- grid.sub(filterChangeSub);
502
+ scroller.setSelectionData(df, seqCol, sh);
417
503
 
418
- const initializeHeaders = (monomerLib: IMonomerLib) => {
419
- const tracks: { id: string, track: MSAHeaderTrack, priority: number }[] = [];
504
+ if (maxSeqLen > 50) {
505
+ grid.props.colHeaderHeight = initialHeaderHeight;
420
506
 
421
- // Create lazy tracks only for MSA sequences
507
+ // Set column width
508
+ setTimeout(() => {
509
+ if (grid.isDetached) return;
510
+ gCol.width = 400;
511
+ }, 300);
512
+ }
422
513
 
423
- // OPTIMIZED: Pass seqHandler directly instead of column/splitter
424
- const conservationTrack = new LazyConservationTrack(
425
- sh,
426
- maxSeqLen,
427
- 45, // DEFAULT_TRACK_HEIGHT
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
- scroller.setupTooltipHandling();
520
+ const cellBounds = e.bounds;
521
+ if (!cellBounds) return;
481
522
 
482
- // Add tracks to scroller
483
- tracks.forEach(({id, track}) => {
484
- scroller.addTrack(id, track);
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
- scroller.setSelectionData(df, seqCol, sh);
528
+ const start = getStart();
488
529
 
489
- if (maxSeqLen > 50) {
490
- grid.props.colHeaderHeight = initialHeaderHeight;
491
530
 
492
- // Set column width
493
- setTimeout(() => {
494
- if (grid.isDetached) return;
495
- gCol.width = 400;
496
- }, 300);
497
- }
498
-
499
- // Handle cell rendering for MSA
500
- grid.sub(grid.onCellRender.subscribe((e) => {
501
- const cell = e.cell;
502
- if (!cell || !cell.isColHeader || cell?.gridColumn?.name !== gCol?.name)
503
- return;
504
-
505
- const cellBounds = e.bounds;
506
- if (!cellBounds) return;
507
-
508
- // Set dynamic properties
509
- scroller.headerHeight = cellBounds.height;
510
- const font = getFontSize();
511
- scroller.positionWidth = font + 8; // MSA always has padding
512
-
513
- const start = getStart();
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
  };