@datagrok/bio 2.25.1 → 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.
@@ -12,6 +12,13 @@ export namespace scripts {
12
12
  return await grok.functions.call('Bio:Embed', { molecule });
13
13
  }
14
14
 
15
+ /**
16
+ Converts molecules to HELM notation based on monomer library
17
+ */
18
+ export async function molToHelmConverterPy(moleculesDataframe: DG.DataFrame , moleculesColumn: DG.Column , libraryJSON: string ): Promise<DG.DataFrame> {
19
+ return await grok.functions.call('Bio:MolToHelmConverterPy', { moleculesDataframe, moleculesColumn, libraryJSON });
20
+ }
21
+
15
22
  /**
16
23
  Create the model peptides/DNA sequences with peptides data
17
24
  */
@@ -172,6 +179,13 @@ export namespace funcs {
172
179
  return await grok.functions.call('Bio:SequenceSpaceTopMenu', { table, molecules, methodName, similarityMetric, plotEmbeddings, preprocessingFunction, options, clusterEmbeddings, isDemo });
173
180
  }
174
181
 
182
+ /**
183
+ Converts Peptide molecules to HELM notation by matching with monomer library
184
+ */
185
+ export async function moleculesToHelmTopMenu(table: DG.DataFrame , molecules: DG.Column ): Promise<void> {
186
+ return await grok.functions.call('Bio:MoleculesToHelmTopMenu', { table, molecules });
187
+ }
188
+
175
189
  /**
176
190
  Converts sequences to molblocks
177
191
  */
package/src/package.g.ts CHANGED
@@ -264,6 +264,15 @@ export async function sequenceSpaceTopMenu(table: DG.DataFrame, molecules: DG.Co
264
264
  return await PackageFunctions.sequenceSpaceTopMenu(table, molecules, methodName, similarityMetric, plotEmbeddings, preprocessingFunction, options, clusterEmbeddings, isDemo);
265
265
  }
266
266
 
267
+ //name: Molecules to HELM
268
+ //description: Converts Peptide molecules to HELM notation by matching with monomer library
269
+ //input: dataframe table { description: Input data table }
270
+ //input: column molecules { semType: Molecule; description: Molecule column }
271
+ //top-menu: Bio | Transform | Molecules to HELM...
272
+ export async function moleculesToHelmTopMenu(table: DG.DataFrame, molecules: DG.Column) : Promise<void> {
273
+ await PackageFunctions.moleculesToHelmTopMenu(table, molecules);
274
+ }
275
+
267
276
  //name: To Atomic Level
268
277
  //description: Converts sequences to molblocks
269
278
  //input: dataframe table { description: Input data table }
package/src/package.ts CHANGED
@@ -78,10 +78,12 @@ import {molecular3DStructureWidget, toAtomicLevelWidget} from './widgets/to-atom
78
78
  import {handleSequenceHeaderRendering} from './widgets/sequence-scrolling-widget';
79
79
  import {PolymerType} from '@datagrok-libraries/js-draw-lite/src/types/org';
80
80
  import {BilnNotationProvider} from './utils/biln';
81
- import {MonomerLibFromFilesProvider} from './utils/monomer-lib/library-file-manager/monomers-lib-provider';
81
+
82
+ import * as api from './package-api';
82
83
  export const _package = new BioPackage(/*{debug: true}/**/);
83
84
  export * from './package.g';
84
85
 
86
+
85
87
  // /** Avoid reassigning {@link monomerLib} because consumers subscribe to {@link IMonomerLib.onChanged} event */
86
88
  // let monomerLib: MonomerLib | null = null;
87
89
  let initBioPromise: Promise<void> | null = null;
@@ -617,6 +619,30 @@ export class PackageFunctions {
617
619
  return res;
618
620
  }
619
621
 
622
+ @grok.decorators.func({
623
+ name: 'Molecules to HELM',
624
+ 'top-menu': 'Bio | Transform | Molecules to HELM...',
625
+ description: 'Converts Peptide molecules to HELM notation by matching with monomer library',
626
+ })
627
+ static async moleculesToHelmTopMenu(
628
+ @grok.decorators.param({name: 'table', options: {description: 'Input data table'}})table: DG.DataFrame,
629
+ @grok.decorators.param({name: 'molecules', options: {semType: 'Molecule', description: 'Molecule column'}})molecules: DG.Column,
630
+ ) {
631
+ // collect current monomer library
632
+ const monomerLib = _package.monomerLib;
633
+ const libJSON = JSON.stringify(monomerLib.toJSON());
634
+ await api.scripts.molToHelmConverterPy(table, molecules, libJSON);
635
+
636
+ // semtype is not automatically set, so we set it manually
637
+ const newCol = table.columns.toList().find((c) => c.name.toLowerCase().includes('regenerated sequence') && c.semType !== DG.SEMTYPE.MACROMOLECULE);
638
+ if (newCol) {
639
+ newCol.meta.units = NOTATION.HELM;
640
+ newCol.semType = DG.SEMTYPE.MACROMOLECULE;
641
+ newCol.setTag('cell.renderer', 'helm');
642
+ }
643
+ }
644
+
645
+
620
646
  @grok.decorators.func({
621
647
  name: 'To Atomic Level',
622
648
  description: 'Converts sequences to molblocks',
@@ -1,3 +1,5 @@
1
+ /* eslint-disable rxjs/no-async-subscribe */
2
+ /* eslint-disable rxjs/no-ignored-subscription */
1
3
  /* eslint-disable max-lines */
2
4
  /* Do not change these import lines to match external modules in webpack configuration */
3
5
  import * as grok from 'datagrok-api/grok';
@@ -62,24 +62,44 @@ export async function standardiseMonomers(monomers: Monomer[]) {
62
62
 
63
63
  /// matches molecules in the dataframe with monomers in the library by canonical smiles
64
64
  export async function matchMoleculesWithMonomers(molDf: DG.DataFrame, molColName: string, monomerLib: IMonomerLib, polymerType: PolymerType = 'PEPTIDE'): Promise<DG.DataFrame> {
65
+ const duplicates = monomerLib.duplicateMonomers?.[polymerType] ?? {};
65
66
  const converterFunc = DG.Func.find({package: 'Chem', name: 'convertMoleculeNotation'})[0];
66
67
  if (!converterFunc)
67
68
  throw new Error('Function convertMoleculeNotation not found, please install Chem package');
68
69
  // first: stamdardize monomers
69
70
  const monomers = monomerLib.getMonomerSymbolsByType(polymerType).map((s) => monomerLib.getMonomer(polymerType, s)!).filter((m) => m && (m.smiles || m.molfile));
70
71
  const fixedMonomers = await standardiseMonomers(monomers);
71
- const cappedSmilse = fixedMonomers.map((m, i) => ({sym: m.symbol, smiles: capSmiles(m.smiles ?? '', m.rgroups ?? []), original: m.smiles, source: monomers[i]?.lib?.source})).filter((s) => !!s?.smiles && !s.smiles.includes('[*:'));
72
+ const unCappedMonomerSmilesMap = fixedMonomers.filter((m) => !!m.smiles).reduce((acc, m) => {
73
+ acc[m.smiles] = {symbol: m.symbol, smiles: m.smiles, original: m.smiles, source: m.lib?.source}; return acc;
74
+ }, {} as {[smiles: string]: {symbol: string, smiles: string, original: string | undefined, source: string | undefined}});
75
+ const cappedMonomerSmiles = fixedMonomers.map((m, i) => ({symbol: m.symbol, smiles: capSmiles(m.smiles ?? '', m.rgroups ?? []), original: m.smiles, source: monomers[i]?.lib?.source}))
76
+ .filter((s) => !!s?.smiles && !s.smiles.includes('[*:'));
77
+
72
78
  // canonicalize all monomer smiles
73
- const monomerSmilesCol = DG.Column.fromList(DG.COLUMN_TYPE.STRING, 'MonomerSmiles', cappedSmilse.map((m) => m.smiles!));
79
+ const monomerSmilesCol = DG.Column.fromList(DG.COLUMN_TYPE.STRING, 'MonomerSmiles', cappedMonomerSmiles.map((m) => m.smiles!));
74
80
  monomerSmilesCol.semType = DG.SEMTYPE.MOLECULE;
75
81
  const canonicalizedMonomersSmilesCol: DG.Column = await converterFunc.apply({molecule: monomerSmilesCol, targetNotation: DG.chem.Notation.Smiles});
76
82
  if (!canonicalizedMonomersSmilesCol || canonicalizedMonomersSmilesCol.length !== monomerSmilesCol.length)
77
83
  throw new Error('Error canonicalizing monomer smiles');
78
- canonicalizedMonomersSmilesCol.toList().forEach((s, i) => cappedSmilse[i].smiles = s);
84
+ canonicalizedMonomersSmilesCol.toList().forEach((s, i) => cappedMonomerSmiles[i].smiles = s);
85
+ const cappedMonomerSmilesMap = cappedMonomerSmiles.reduce((acc, m) => { acc[m.smiles] = m; return acc; }, {} as {[smiles: string]: {symbol: string, smiles: string, original: string | undefined, source: string | undefined}});
79
86
 
80
- const molecules = molDf.col(molColName)!;
81
- const canonicalizedMoleculesCol: DG.Column = await converterFunc.apply({molecule: molecules, targetNotation: DG.chem.Notation.Smiles});
82
- if (!canonicalizedMoleculesCol || canonicalizedMoleculesCol.length !== molecules.length)
87
+ const moleculesOriginalCol = molDf.col(molColName)!;
88
+ const correctedOriginalList = moleculesOriginalCol.toList().map((s) => {
89
+ if (!s) return s;
90
+ try {
91
+ const isMolBlock = s.includes('\n');
92
+ return getCorrectedSmiles([], isMolBlock ? undefined : s, isMolBlock ? s : undefined);
93
+ } catch (_e) {
94
+ return s;
95
+ }
96
+ });
97
+ const moleculesOriginalColCorrected = DG.Column.fromList(DG.COLUMN_TYPE.STRING, 'MoleculesOriginalCorrected', correctedOriginalList);
98
+ // create dummy df
99
+ moleculesOriginalColCorrected.semType = DG.SEMTYPE.MOLECULE;
100
+ const _ddf = DG.DataFrame.fromColumns([moleculesOriginalColCorrected]);
101
+ const canonicalizedMoleculesCol: DG.Column = await converterFunc.apply({molecule: moleculesOriginalColCorrected, targetNotation: DG.chem.Notation.Smiles});
102
+ if (!canonicalizedMoleculesCol || canonicalizedMoleculesCol.length !== moleculesOriginalColCorrected.length)
83
103
  throw new Error('Error canonicalizing molecules');
84
104
 
85
105
  const canonicalizedMolecules = canonicalizedMoleculesCol.toList();
@@ -95,13 +115,14 @@ export async function matchMoleculesWithMonomers(molDf: DG.DataFrame, molColName
95
115
  for (let i = 0; i < canonicalizedMolecules.length; i++) {
96
116
  const mol = canonicalizedMolecules[i];
97
117
  if (!mol) continue;
98
- for (let j = 0; j < cappedSmilse.length; j++) {
99
- if (cappedSmilse[j].smiles === mol) {
100
- matchingMonomerSmilesCol.set(i, cappedSmilse[j].original!, false);
101
- matchingMonomerSymbolCol.set(i, cappedSmilse[j].sym, false);
102
- sourceLibCol.set(i, cappedSmilse[j].source ?? '', false);
103
- break;
104
- }
118
+ const match = cappedMonomerSmilesMap[mol] ?? unCappedMonomerSmilesMap[mol];
119
+ if (match) {
120
+ const matchSymbol = match.symbol;
121
+ const sources = (duplicates[matchSymbol]?.length ?? 0) > 0 ? duplicates[matchSymbol].map((m) => m?.lib?.source).filter((s) => !!s).join(', ') : (match.source ?? '');
122
+ const originalSmiles = match.original ?? match.smiles;
123
+ matchingMonomerSmilesCol.set(i, originalSmiles, false);
124
+ matchingMonomerSymbolCol.set(i, matchSymbol, false);
125
+ sourceLibCol.set(i, sources, false);
105
126
  }
106
127
  }
107
128
  return resultDf;
@@ -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
  };