@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.
- package/CHANGELOG.md +7 -0
- 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 +2 -0
- package/src/utils/monomer-lib/monomer-manager/monomer-manager.ts +34 -13
- package/src/widgets/sequence-scrolling-widget.ts +195 -183
- package/test-console-output-1.log +338 -342
- package/test-record-1.mp4 +0 -0
package/src/package-api.ts
CHANGED
|
@@ -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
|
-
|
|
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',
|
|
@@ -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
|
|
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',
|
|
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) =>
|
|
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
|
|
81
|
-
const
|
|
82
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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
|
};
|