@datagrok/bio 2.26.7 → 2.26.8

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/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "Davit Rizhinashvili",
6
6
  "email": "drizhinashvili@datagrok.ai"
7
7
  },
8
- "version": "2.26.7",
8
+ "version": "2.26.8",
9
9
  "description": "Bioinformatics support (import/export of sequences, conversion, visualization, analysis). [See more](https://github.com/datagrok-ai/public/blob/master/packages/Bio/README.md) for details.",
10
10
  "repository": {
11
11
  "type": "git",
package/src/package.ts CHANGED
@@ -390,7 +390,7 @@ export class PackageFunctions {
390
390
  })
391
391
  static monomerInfoPanel(
392
392
  @grok.decorators.param({options: {semType: 'Monomer'}}) monomerSv: DG.SemanticValue): DG.Widget {
393
- return getMonomerInfoWidget(monomerSv.value as string, _package.monomerLib);
393
+ return getMonomerInfoWidget(monomerSv, _package.monomerLib);
394
394
  }
395
395
 
396
396
  @grok.decorators.func({
@@ -225,8 +225,8 @@ export class MacromoleculeDifferenceCellRendererBack extends CellRendererWithMon
225
225
  const splitter = this.tableCol.temp[SeqTemps.notationProvider]?.separatorSplitter ?? this.tableCol.temp[SeqTemps.notationProvider]?.splitter ?? getSplitter(units, separator);
226
226
  const s1SS = splitter(s1);
227
227
  const s2SS = splitter(s2);
228
- const subParts1 = wu.count(0).take(s1SS.length).map((posIdx) => s1SS.getCanonical(posIdx)).toArray();
229
- const subParts2 = wu.count(0).take(s2SS.length).map((posIdx) => s2SS.getCanonical(posIdx)).toArray();
228
+ const subParts1 = wu.count(0).take(s1SS.length).map((posIdx) => s1SS.getOriginal(posIdx)).toArray();
229
+ const subParts2 = wu.count(0).take(s2SS.length).map((posIdx) => s2SS.getOriginal(posIdx)).toArray();
230
230
  const alphabet = this.tableCol.getTag(bioTAGS.alphabet);
231
231
  const biotype = alphabet === ALPHABET.RNA || alphabet === ALPHABET.DNA ? HelmTypes.NUCLEOTIDE : HelmTypes.AA;
232
232
  drawMoleculeDifferenceOnCanvas(g, x, y, w, h, subParts1, subParts2, biotype, this.monomerLib, undefined, undefined);
@@ -154,7 +154,7 @@ export class MonomerCellRendererBack extends CellRendererWithMonomerLibBackBase
154
154
  const actBioType: HelmType = this.getHelmType(gridCell, biotype);
155
155
  return this.monomerLib!.getTooltip(actBioType, s);
156
156
  });
157
- const tooltipEl = ui.divH(tooltipEls, {style: {alignItems: 'top'}});
157
+ const tooltipEl = ui.divH(tooltipEls, {style: {alignItems: 'top', flexWrap: 'wrap', gap: '4px'}});
158
158
  // tooltip max width is 600px, so we need to shrink the canvases a bit if needed. by default, it is 250px
159
159
  const canvases = Array.from(tooltipEl.querySelectorAll('canvas'));
160
160
  if (canvases.length > 2) {
@@ -5,14 +5,11 @@ import * as DG from 'datagrok-api/dg';
5
5
 
6
6
  import {IMonomerLib, Monomer} from '@datagrok-libraries/bio/src/types/monomer-library';
7
7
  import {HELM_REQUIRED_FIELD as REQ, HELM_RGROUP_FIELDS as RGP} from '@datagrok-libraries/bio/src/utils/const';
8
+ import {MONOMER_MOTIF_SPLITTER, MONOMER_CANONICALIZER_TEMP} from '@datagrok-libraries/bio/src/utils/macromolecule/consts';
9
+ import {IMonomerCanonicalizer} from '@datagrok-libraries/bio/src/utils/macromolecule/types';
8
10
 
9
11
  import {getCorrectedSmiles, capSmiles} from '../utils/monomer-lib/monomer-manager/monomer-manager';
10
12
 
11
- /** Finds a monomer in the library across all polymer types. */
12
- function findMonomer(monomerLib: IMonomerLib, symbol: string): Monomer | null {
13
- return monomerLib.getMonomer(null, symbol);
14
- }
15
-
16
13
  /** Caps the monomer (replaces R-groups with cap atoms) and returns capped SMILES. */
17
14
  function getCappedSmiles(monomer: Monomer): string | null {
18
15
  try {
@@ -22,58 +19,101 @@ function getCappedSmiles(monomer: Monomer): string | null {
22
19
  return null;
23
20
  }
24
21
 
22
+ /** Renders a single-monomer details pane content. */
23
+ function renderMonomerDetails(monomer: Monomer): HTMLElement {
24
+ const map: {[key: string]: any} = {
25
+ 'Symbol': monomer[REQ.SYMBOL],
26
+ 'Name': monomer[REQ.NAME],
27
+ 'Polymer Type': monomer[REQ.POLYMER_TYPE],
28
+ 'Monomer Type': monomer[REQ.MONOMER_TYPE],
29
+ };
30
+ if (monomer[REQ.AUTHOR])
31
+ map['Author'] = monomer[REQ.AUTHOR];
32
+ if (monomer.naturalAnalog)
33
+ map['Natural Analog'] = monomer.naturalAnalog;
34
+ if (monomer.lib?.source) {
35
+ let source = monomer.lib.source;
36
+ if (source.endsWith('.json'))
37
+ source = source.substring(0, source.length - 5);
38
+ map['Library'] = source;
39
+ }
40
+
41
+ // Structure
42
+ if (monomer.molfile)
43
+ map['Structure'] = grok.chem.drawMolecule(monomer.molfile, 150, 150);
44
+ else if (monomer.smiles)
45
+ map['Structure'] = grok.chem.drawMolecule(monomer.smiles, 150, 150);
46
+
47
+ // R-Groups
48
+ const rgroups = monomer[REQ.RGROUPS];
49
+ if (rgroups && rgroups.length > 0) {
50
+ for (const rg of rgroups)
51
+ map[rg[RGP.LABEL] ?? '?'] = rg[RGP.ALTERNATE_ID] ?? '';
52
+ }
53
+
54
+ return ui.tableFromMap(map);
55
+ }
56
+
57
+ /** Renders the molecule info panel pane content for a single monomer. */
58
+ function renderMoleculePane(monomer: Monomer): HTMLElement {
59
+ const cappedMol = getCappedSmiles(monomer);
60
+ if (!cappedMol)
61
+ return ui.divText('No molecular structure available');
62
+
63
+ const molSv = DG.SemanticValue.fromValueType(cappedMol, DG.SEMTYPE.MOLECULE);
64
+ return ui.panels.infoPanel(molSv).root;
65
+ }
66
+
67
+ /** Tries to get the canonicalizer from the SemanticValue's cell column. */
68
+ function getCanonicalizer(sv: DG.SemanticValue): IMonomerCanonicalizer | null {
69
+ try {
70
+ const col = sv.cell?.column;
71
+ if (col)
72
+ return col.temp[MONOMER_CANONICALIZER_TEMP] ?? null;
73
+ } catch { /* no cell context */ }
74
+ return null;
75
+ }
76
+
25
77
  /** Creates a widget for the monomer info panel shown in the context panel. */
26
- export function getMonomerInfoWidget(symbol: string, monomerLib: IMonomerLib): DG.Widget {
27
- const monomer = findMonomer(monomerLib, symbol);
28
- if (!monomer)
29
- return new DG.Widget(ui.divText(`Monomer '${symbol}' not found in the library.`));
78
+ export function getMonomerInfoWidget(sv: DG.SemanticValue, monomerLib: IMonomerLib): DG.Widget {
79
+ const rawValue = sv.value as string;
80
+ if (!rawValue)
81
+ return new DG.Widget(ui.divText('No monomer value.'));
30
82
 
31
- const acc = ui.accordion('Monomer');
83
+ // Canonicalize if a canonicalizer is available from the column context
84
+ const canonicalizer = getCanonicalizer(sv);
85
+ const canonicalized = canonicalizer ? canonicalizer.canonicalize(rawValue) : rawValue;
32
86
 
33
- // Details pane includes general info, structure, and R-groups in one table
34
- acc.addPane('Details', () => {
35
- const map: {[key: string]: any} = {
36
- 'Symbol': monomer[REQ.SYMBOL],
37
- 'Name': monomer[REQ.NAME],
38
- 'Polymer Type': monomer[REQ.POLYMER_TYPE],
39
- 'Monomer Type': monomer[REQ.MONOMER_TYPE],
40
- };
41
- if (monomer[REQ.AUTHOR])
42
- map['Author'] = monomer[REQ.AUTHOR];
43
- if (monomer.naturalAnalog)
44
- map['Natural Analog'] = monomer.naturalAnalog;
45
- if (monomer.lib?.source) {
46
- let source = monomer.lib.source;
47
- if (source.endsWith('.json'))
48
- source = source.substring(0, source.length - 5);
49
- map['Library'] = source;
50
- }
87
+ // Split by motif splitter a cell value may contain multiple monomers
88
+ const symbols = canonicalized.split(MONOMER_MOTIF_SPLITTER).map((s) => s.trim()).filter((s) => s.length > 0);
51
89
 
52
- // Structure
53
- if (monomer.molfile)
54
- map['Structure'] = grok.chem.drawMolecule(monomer.molfile, 150, 150);
55
- else if (monomer.smiles)
56
- map['Structure'] = grok.chem.drawMolecule(monomer.smiles, 150, 150);
57
-
58
- // R-Groups
59
- const rgroups = monomer[REQ.RGROUPS];
60
- if (rgroups && rgroups.length > 0) {
61
- for (const rg of rgroups)
62
- map[rg[RGP.LABEL] ?? '?'] = rg[RGP.ALTERNATE_ID] ?? '';
63
- }
90
+ // Resolve monomers from library
91
+ const monomers: {symbol: string; monomer: Monomer | null}[] =
92
+ symbols.map((s) => ({symbol: s, monomer: monomerLib.getMonomer(null, s)}));
64
93
 
65
- return ui.tableFromMap(map);
66
- }, true);
94
+ const found = monomers.filter((m) => m.monomer !== null);
95
+ if (found.length === 0)
96
+ return new DG.Widget(ui.divText(`Monomer '${rawValue}' not found in the library.`));
67
97
 
68
- // Molecule panel pane — cap the monomer first, then embed the generic Molecule context panel
69
- acc.addPane('Molecule', () => {
70
- const cappedMol = getCappedSmiles(monomer);
71
- if (!cappedMol)
72
- return ui.divText('No molecular structure available');
98
+ const acc = ui.accordion('Monomer');
73
99
 
74
- const molSv = DG.SemanticValue.fromValueType(cappedMol, DG.SEMTYPE.MOLECULE);
75
- return ui.panels.infoPanel(molSv).root;
76
- }, true);
100
+ if (found.length === 1) {
101
+ // Single monomer — flat panes
102
+ const m = found[0].monomer!;
103
+ acc.addPane('Details', () => renderMonomerDetails(m), true);
104
+ acc.addPane('Molecule', () => renderMoleculePane(m), true);
105
+ } else {
106
+ // Multiple monomers — one pane per monomer
107
+ for (const {symbol, monomer} of found) {
108
+ acc.addPane(symbol, () => {
109
+ return ui.divV([
110
+ renderMonomerDetails(monomer!),
111
+ ui.element('hr'),
112
+ renderMoleculePane(monomer!),
113
+ ]);
114
+ }, true);
115
+ }
116
+ }
77
117
 
78
118
  return new DG.Widget(acc.root);
79
119
  }