@datagrok/sequence-translator 1.10.13 → 1.10.15
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 +6 -0
- package/detectors.js +30 -2
- package/dist/455.js +1 -1
- package/dist/455.js.map +1 -1
- package/dist/package-test.js +1 -1
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/files/samples/sirna-demo.csv +38 -0
- package/files/tests/chem_enum_cores.csv +5 -0
- package/files/tests/chem_enum_rgroups.csv +5 -0
- package/package.json +2 -2
- package/src/apps/structure/view/ui.ts +1 -1
- package/src/apps/translator/view/ui.ts +1 -1
- package/src/oligo-renderer/canvas-renderer.ts +500 -0
- package/src/oligo-renderer/cell-renderer.ts +105 -0
- package/src/oligo-renderer/converters.ts +77 -0
- package/src/oligo-renderer/helm-parser.ts +154 -0
- package/src/oligo-renderer/legend-panel.ts +154 -0
- package/src/oligo-renderer/structures-panel.ts +96 -0
- package/src/oligo-renderer/tooltip.ts +223 -0
- package/src/oligo-renderer/types.ts +221 -0
- package/src/package-api.ts +43 -1
- package/src/package-test.ts +2 -0
- package/src/package.g.ts +56 -3
- package/src/package.ts +92 -5
- package/src/polytool/const.ts +1 -1
- package/src/polytool/pt-chem-enum-dialog.ts +940 -0
- package/src/polytool/pt-chem-enum.ts +553 -0
- package/src/polytool/pt-dialog.ts +4 -125
- package/src/polytool/pt-enumerate-seq-dialog.ts +3 -3
- package/src/tests/oligo-renderer-tests.ts +299 -0
- package/src/tests/polytool-enumerate-chem-tests.ts +408 -0
- package/test-console-output-1.log +303 -97
- package/test-record-1.mp4 +0 -0
- package/src/polytool/pt-enumeration-chem.ts +0 -100
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hover tooltip for OligoNucleotide cells.
|
|
3
|
+
*
|
|
4
|
+
* Shows monomer details synchronously, then async-loads RDKit structures
|
|
5
|
+
* for whatever was hovered:
|
|
6
|
+
* - Nucleotide chip → sugar + base + 3'-phosphate (3 small structures)
|
|
7
|
+
* - PS / phosphate-mod linkage in the gap → just the phosphate structure
|
|
8
|
+
* - Conjugate pill → that conjugate's structure
|
|
9
|
+
*
|
|
10
|
+
* Structures come from the **central Bio monomer library** (not the
|
|
11
|
+
* SequenceTranslator-internal one). Symbols are resolved via the alias map
|
|
12
|
+
* so legacy / vendor shorthand still finds the canonical entry.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as grok from 'datagrok-api/grok';
|
|
16
|
+
import * as ui from 'datagrok-api/ui';
|
|
17
|
+
|
|
18
|
+
import {getMonomerLibHelper, IMonomerLib} from '@datagrok-libraries/bio/src/types/monomer-library';
|
|
19
|
+
|
|
20
|
+
import {HitResult} from './canvas-renderer';
|
|
21
|
+
import {
|
|
22
|
+
canonicalPhosphateSymbol, canonicalSugarSymbol,
|
|
23
|
+
ParsedNucleotide, resolveConjugate, resolvePhosphate, resolveSugar,
|
|
24
|
+
} from './types';
|
|
25
|
+
|
|
26
|
+
const STRUCT_W = 110;
|
|
27
|
+
const STRUCT_H = 90;
|
|
28
|
+
|
|
29
|
+
type StructureKind = 'sugar' | 'base' | 'phosphate' | 'conjugate';
|
|
30
|
+
|
|
31
|
+
/** Show a tooltip for the hovered hit. Idempotent — replaces any current tooltip. */
|
|
32
|
+
export function showMonomerTooltip(hit: HitResult, clientX: number, clientY: number): void {
|
|
33
|
+
const root = buildTooltipRoot(hit);
|
|
34
|
+
ui.tooltip.show(root, clientX + 14, clientY + 14);
|
|
35
|
+
void renderStructuresAsync(root, hit);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildTooltipRoot(hit: HitResult): HTMLElement {
|
|
39
|
+
const m = hit.monomer;
|
|
40
|
+
const title = ui.divText('', {style: {fontWeight: '600', marginBottom: '4px'}});
|
|
41
|
+
const detailRows: HTMLElement[] = [];
|
|
42
|
+
|
|
43
|
+
// Linkage hover (PS marker in the gap) — shape the tooltip around the linkage
|
|
44
|
+
if (hit.linkage) {
|
|
45
|
+
const phos = resolvePhosphate(hit.linkage.phosphateSymbol);
|
|
46
|
+
title.textContent = `${phos.meta.name} linkage`;
|
|
47
|
+
detailRows.push(
|
|
48
|
+
kv('Strand', hit.strand === 'sense' ? 'Sense' : 'Antisense'),
|
|
49
|
+
kv('Between', `pos ${hit.position + 1} – ${hit.position + 2}`),
|
|
50
|
+
kv('HELM symbol', `[${canonicalPhosphateSymbol(hit.linkage.phosphateSymbol)}]`, true),
|
|
51
|
+
);
|
|
52
|
+
} else if (m.kind === 'nucleotide') {
|
|
53
|
+
const nt = m as ParsedNucleotide;
|
|
54
|
+
const sugarRes = resolveSugar(nt.sugar, nt.base);
|
|
55
|
+
const phosRes = resolvePhosphate(nt.phosphate);
|
|
56
|
+
title.textContent = `${nt.base ?? '?'}${hit.position + 1} — ${sugarRes.meta.name}`;
|
|
57
|
+
detailRows.push(
|
|
58
|
+
kv('Strand', hit.strand === 'sense' ? 'Sense' : 'Antisense'),
|
|
59
|
+
kv('Position', String(hit.position + 1)),
|
|
60
|
+
kv('Base', nt.base ?? '—'),
|
|
61
|
+
kv('Sugar', sugarRes.meta.name),
|
|
62
|
+
kv('3\'-linkage', phosRes.meta.name),
|
|
63
|
+
kv('HELM', nt.raw, true),
|
|
64
|
+
);
|
|
65
|
+
} else {
|
|
66
|
+
const conj = resolveConjugate(m.symbol);
|
|
67
|
+
title.textContent = conj.meta.name;
|
|
68
|
+
detailRows.push(
|
|
69
|
+
kv('Strand', hit.strand === 'sense' ? 'Sense' : 'Antisense'),
|
|
70
|
+
kv('Position', String(hit.position + 1)),
|
|
71
|
+
kv('Type', 'Terminal conjugate / linker'),
|
|
72
|
+
kv('HELM', m.raw, true),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Reserved area for structures — populated async.
|
|
77
|
+
// Holds 1 (linkage / conjugate) or 3 (full nucleotide) small canvases side by side.
|
|
78
|
+
const structHost = ui.div([], {style: {
|
|
79
|
+
marginTop: '8px',
|
|
80
|
+
display: 'flex', flexDirection: 'row', gap: '6px',
|
|
81
|
+
flexWrap: 'wrap',
|
|
82
|
+
}});
|
|
83
|
+
structHost.dataset.role = 'struct';
|
|
84
|
+
const placeholder = ui.divText('Loading structures…', {style: {
|
|
85
|
+
fontSize: '11px', color: 'var(--grey-4, #888)',
|
|
86
|
+
}});
|
|
87
|
+
structHost.appendChild(placeholder);
|
|
88
|
+
|
|
89
|
+
return ui.divV([title, ...detailRows, structHost], {
|
|
90
|
+
style: {fontSize: '12px', lineHeight: '1.45', minWidth: '240px', maxWidth: '380px'},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function kv(k: string, v: string, mono = false): HTMLElement {
|
|
95
|
+
return ui.divH([
|
|
96
|
+
ui.divText(k, {style: {color: 'var(--grey-4, #888)', width: '90px', flexShrink: '0'}}),
|
|
97
|
+
ui.divText(v, {style: mono ? {fontFamily: 'ui-monospace, Menlo, monospace', wordBreak: 'break-all'} : {}}),
|
|
98
|
+
], {style: {gap: '6px'}});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Cached merged Bio monomer library. */
|
|
102
|
+
let _bioLibPromise: Promise<IMonomerLib> | null = null;
|
|
103
|
+
function getBioLib(): Promise<IMonomerLib> {
|
|
104
|
+
if (!_bioLibPromise) {
|
|
105
|
+
_bioLibPromise = (async () => {
|
|
106
|
+
const helper = await getMonomerLibHelper();
|
|
107
|
+
return helper.getMonomerLib();
|
|
108
|
+
})();
|
|
109
|
+
}
|
|
110
|
+
return _bioLibPromise;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Try the original symbol first, then the canonical alias, across all polymer types. */
|
|
114
|
+
function findMonomerMolfile(lib: IMonomerLib, rawSymbol: string, kind: StructureKind): string | null {
|
|
115
|
+
const candidates: string[] = [rawSymbol];
|
|
116
|
+
if (kind === 'sugar') candidates.push(canonicalSugarSymbol(rawSymbol));
|
|
117
|
+
else if (kind === 'phosphate') candidates.push(canonicalPhosphateSymbol(rawSymbol));
|
|
118
|
+
// Bases (A/C/G/U/T) and conjugates: try the symbol as-is.
|
|
119
|
+
|
|
120
|
+
const polymerTypes = lib.getPolymerTypes();
|
|
121
|
+
for (const sym of candidates) {
|
|
122
|
+
for (const pt of polymerTypes) {
|
|
123
|
+
const monomer = lib.getMonomer(pt, sym);
|
|
124
|
+
if (monomer && monomer.molfile) return monomer.molfile;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function renderStructuresAsync(root: HTMLElement, hit: HitResult): Promise<void> {
|
|
131
|
+
const host = root.querySelector('[data-role="struct"]') as HTMLDivElement | null;
|
|
132
|
+
if (!host) return;
|
|
133
|
+
|
|
134
|
+
// Determine which structures to show
|
|
135
|
+
let requests: { label: string; symbol: string; kind: StructureKind }[];
|
|
136
|
+
if (hit.linkage) {
|
|
137
|
+
requests = [{label: 'Linkage', symbol: hit.linkage.phosphateSymbol, kind: 'phosphate'}];
|
|
138
|
+
} else if (hit.monomer.kind === 'nucleotide') {
|
|
139
|
+
const nt = hit.monomer as ParsedNucleotide;
|
|
140
|
+
requests = [
|
|
141
|
+
{label: 'Sugar', symbol: nt.sugar, kind: 'sugar'},
|
|
142
|
+
];
|
|
143
|
+
if (nt.base) requests.push({label: 'Base', symbol: nt.base, kind: 'base'});
|
|
144
|
+
if (nt.phosphate) requests.push({label: '3\'-linkage', symbol: nt.phosphate, kind: 'phosphate'});
|
|
145
|
+
} else {
|
|
146
|
+
requests = [{label: 'Conjugate', symbol: hit.monomer.symbol, kind: 'conjugate'}];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let lib: IMonomerLib;
|
|
150
|
+
try {
|
|
151
|
+
lib = await getBioLib();
|
|
152
|
+
} catch {
|
|
153
|
+
host.innerHTML = '';
|
|
154
|
+
host.appendChild(ui.divText('Monomer library unavailable', {
|
|
155
|
+
style: {fontSize: '11px', color: 'var(--grey-4, #888)'},
|
|
156
|
+
}));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
host.innerHTML = '';
|
|
161
|
+
|
|
162
|
+
for (const req of requests) {
|
|
163
|
+
const cell = makeStructureCell(req.label);
|
|
164
|
+
host.appendChild(cell.root);
|
|
165
|
+
const molfile = findMonomerMolfile(lib, req.symbol, req.kind);
|
|
166
|
+
if (!molfile) {
|
|
167
|
+
cell.body.textContent = `Not in HELMCore: ${req.symbol}`;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
void drawMolfileCached(cell.body, molfile, cacheKeyFor(req));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Cache key for a structure render. */
|
|
175
|
+
function cacheKeyFor(req: {kind: StructureKind; symbol: string}): string {
|
|
176
|
+
let canonical = req.symbol;
|
|
177
|
+
if (req.kind === 'sugar') canonical = canonicalSugarSymbol(req.symbol);
|
|
178
|
+
else if (req.kind === 'phosphate') canonical = canonicalPhosphateSymbol(req.symbol);
|
|
179
|
+
return `${req.kind}:${canonical}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function makeStructureCell(label: string): { root: HTMLElement; body: HTMLDivElement } {
|
|
183
|
+
const body = ui.div([], {style: {
|
|
184
|
+
// width: `${STRUCT_W}px`, height: `${STRUCT_H}px`,
|
|
185
|
+
// background: 'var(--grey-1, #f4f4f4)', border: '1px solid var(--grey-2, #ddd)',
|
|
186
|
+
borderRadius: '4px',
|
|
187
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
188
|
+
fontSize: '10px', color: 'var(--grey-4, #888)', textAlign: 'center', padding: '4px',
|
|
189
|
+
}}) as HTMLDivElement;
|
|
190
|
+
const cap = ui.divText(label, {style: {
|
|
191
|
+
fontSize: '10px', color: 'var(--grey-5, #555)', textAlign: 'center', marginTop: '2px',
|
|
192
|
+
letterSpacing: '0.04em', textTransform: 'uppercase', fontWeight: '600',
|
|
193
|
+
}});
|
|
194
|
+
const root = ui.divV([body, cap], {style: {display: 'inline-flex'}});
|
|
195
|
+
return {root, body};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Cache of rendered structures by kind+canonical-symbol.
|
|
199
|
+
*
|
|
200
|
+
* We cache the actual element returned by grok.chem.drawMolecule and reuse
|
|
201
|
+
* the same instance across tooltips. We deliberately do NOT clone — when
|
|
202
|
+
* `drawMolecule` returns a canvas, its bitmap is painted asynchronously
|
|
203
|
+
* (RDKit init etc.), so a clone taken right after the call ends up blank.
|
|
204
|
+
* Since only one tooltip is visible at a time, moving the cached node
|
|
205
|
+
* between tooltip hosts via `appendChild` is safe: the previous host is
|
|
206
|
+
* being torn down. */
|
|
207
|
+
const _structCache = new Map<string, HTMLElement>();
|
|
208
|
+
|
|
209
|
+
function drawMolfileCached(host: HTMLDivElement, molfile: string, cacheKey: string): void {
|
|
210
|
+
try {
|
|
211
|
+
let cached = _structCache.get(cacheKey);
|
|
212
|
+
if (!cached) {
|
|
213
|
+
cached = grok.chem.drawMolecule(molfile, STRUCT_W, STRUCT_H, false);
|
|
214
|
+
if (_structCache.size > 256) _structCache.clear();
|
|
215
|
+
_structCache.set(cacheKey, cached);
|
|
216
|
+
}
|
|
217
|
+
host.innerHTML = '';
|
|
218
|
+
// appendChild moves the node from any previous parent automatically.
|
|
219
|
+
host.appendChild(cached);
|
|
220
|
+
} catch {
|
|
221
|
+
host.textContent = 'Render failed';
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types and modification dictionary for the OligoNucleotide cell renderer.
|
|
3
|
+
*
|
|
4
|
+
* The renderer is intentionally self-contained — it does NOT depend on
|
|
5
|
+
* `_package.initLibData()` having completed, so the grid can render oligo
|
|
6
|
+
* cells before any app has opened. Only the tooltip's RDKit structure
|
|
7
|
+
* depends on the monomer library (which is loaded async there).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const OLIGO_SEM_TYPE = 'OligoNucleotide';
|
|
11
|
+
export const OLIGO_UNITS = 'helm';
|
|
12
|
+
|
|
13
|
+
/** Per-nucleotide parsed model. One per chip in the rendered output. */
|
|
14
|
+
export interface ParsedNucleotide {
|
|
15
|
+
kind: 'nucleotide';
|
|
16
|
+
/** Index inside the strand (0-based). */
|
|
17
|
+
position: number;
|
|
18
|
+
/** Sugar HELM symbol, normalized (no brackets). HELMCore canonical: 'r', 'd', 'm', 'fl2r', 'lna'. */
|
|
19
|
+
sugar: string;
|
|
20
|
+
/** Single-letter base — A / C / G / U / T or null when missing. */
|
|
21
|
+
base: string | null;
|
|
22
|
+
/** Phosphate HELM symbol normalized (no brackets). HELMCore canonical: 'p', 'sp'. Empty for terminal. */
|
|
23
|
+
phosphate: string;
|
|
24
|
+
/** Original HELM monomer text for tooltip + structure lookup. */
|
|
25
|
+
raw: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Terminal conjugate (GalNAc, cholesterol, biotin, L3 linker, …). */
|
|
29
|
+
export interface ParsedConjugate {
|
|
30
|
+
kind: 'conjugate';
|
|
31
|
+
position: number;
|
|
32
|
+
symbol: string;
|
|
33
|
+
raw: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type ParsedMonomer = ParsedNucleotide | ParsedConjugate;
|
|
37
|
+
|
|
38
|
+
export interface ParsedStrand {
|
|
39
|
+
/** 'RNA' | 'DNA' | 'CHEM' */
|
|
40
|
+
type: string;
|
|
41
|
+
monomers: ParsedMonomer[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ParsedDuplex {
|
|
45
|
+
sense: ParsedStrand;
|
|
46
|
+
antisense: ParsedStrand | null;
|
|
47
|
+
/** Original HELM string (kept verbatim for tooltip / debug). */
|
|
48
|
+
raw: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Visual classification used for chip coloring. */
|
|
52
|
+
export type ModCategory = 'sugar' | 'phosphate' | 'conjugate' | 'base';
|
|
53
|
+
|
|
54
|
+
export interface ModMeta {
|
|
55
|
+
/** Full user-facing name, used in tooltip + legend. */
|
|
56
|
+
name: string;
|
|
57
|
+
/** 3–8 char display label for chips that don't show a base letter (conjugates). */
|
|
58
|
+
short: string;
|
|
59
|
+
/** Canonical CSS color. */
|
|
60
|
+
color: string;
|
|
61
|
+
category: ModCategory;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ---------------------------------------------------------------- *
|
|
65
|
+
* Modification dictionary — flat lookups by HELM symbol.
|
|
66
|
+
*
|
|
67
|
+
* Canonical symbols are HELMCore (Pistoia / Bio package):
|
|
68
|
+
* sugars — `r`, `d`, `m`, `25r`, `fl2r`, `lna`, `moe`, ...
|
|
69
|
+
* phosphates — `p`, `sp`, ...
|
|
70
|
+
* bases — `A`, `C`, `G`, `U`, `T`
|
|
71
|
+
*
|
|
72
|
+
* Some vendor / Axolabs-style strings (`R`, `mR`, `fR`, `LR`, `dR`, `sP`)
|
|
73
|
+
* appear in legacy data and in older internal tools. The SYMBOL_ALIASES
|
|
74
|
+
* map normalizes them to canonical at lookup time, so the renderer
|
|
75
|
+
* accepts both. Tooltips / structure look-up use the canonical symbol
|
|
76
|
+
* (which is what the central Bio monomer library actually contains).
|
|
77
|
+
*
|
|
78
|
+
* Anything not in the dictionary falls back to a deterministic hash color.
|
|
79
|
+
* ---------------------------------------------------------------- */
|
|
80
|
+
|
|
81
|
+
export const SUGAR_MODS: Readonly<Record<string, ModMeta>> = Object.freeze({
|
|
82
|
+
// Canonical sugars — chip uses base-canonical color (see BASE_COLORS), not a sugar color
|
|
83
|
+
'r': {name: 'Ribose', short: 'RNA', color: '#D0D0D0', category: 'sugar'},
|
|
84
|
+
'd': {name: 'Deoxyribose', short: 'DNA', color: '#BDBDBD', category: 'sugar'},
|
|
85
|
+
|
|
86
|
+
// 2'-O-Methyl
|
|
87
|
+
'm': {name: '2\'-O-Methyl', short: '2\'-OMe', color: '#4A90E2', category: 'sugar'},
|
|
88
|
+
'25r': {name: '2\'-O-Methyl (2,5)', short: '2\'-OMe', color: '#4A90E2', category: 'sugar'},
|
|
89
|
+
|
|
90
|
+
// 2'-Fluoro
|
|
91
|
+
'fl2r': {name: '2\'-Fluoro', short: '2\'-F', color: '#50C878', category: 'sugar'},
|
|
92
|
+
|
|
93
|
+
// LNA / cEt / ENA — bridged
|
|
94
|
+
'lna': {name: 'LNA', short: 'LNA', color: '#E89D3C', category: 'sugar'},
|
|
95
|
+
'cet': {name: 'cEt', short: 'cEt', color: '#E07B30', category: 'sugar'},
|
|
96
|
+
'ena': {name: 'ENA', short: 'ENA', color: '#D26C20', category: 'sugar'},
|
|
97
|
+
|
|
98
|
+
// 2'-MOE
|
|
99
|
+
'moe': {name: '2\'-MOE', short: '2\'-MOE', color: '#66CDAA', category: 'sugar'},
|
|
100
|
+
|
|
101
|
+
// GNA / UNA / FANA / hexitol-NA
|
|
102
|
+
'Rgna': {name: '(R)-GNA', short: 'GNA', color: '#C7A981', category: 'sugar'},
|
|
103
|
+
'Sgna': {name: '(S)-GNA', short: 'GNA', color: '#C7A981', category: 'sugar'},
|
|
104
|
+
'una': {name: 'UNA', short: 'UNA', color: '#ED68B8', category: 'sugar'},
|
|
105
|
+
'fana': {name: 'FANA', short: 'FANA', color: '#76B947', category: 'sugar'},
|
|
106
|
+
'hna': {name: 'HNA', short: 'HNA', color: '#A4D080', category: 'sugar'},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/** Vendor / legacy / Pistoia-style sugar symbols mapped to canonical HELMCore. */
|
|
110
|
+
export const SUGAR_ALIASES: Readonly<Record<string, string>> = Object.freeze({
|
|
111
|
+
'R': 'r',
|
|
112
|
+
'dR': 'd',
|
|
113
|
+
'mR': 'm',
|
|
114
|
+
'fR': 'fl2r',
|
|
115
|
+
'LR': 'lna',
|
|
116
|
+
'L': 'lna',
|
|
117
|
+
'MOE': 'moe',
|
|
118
|
+
'GNA': 'Sgna',
|
|
119
|
+
'UNA': 'una',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
export const PHOSPHATE_MODS: Readonly<Record<string, ModMeta>> = Object.freeze({
|
|
123
|
+
'p': {name: 'Phosphate', short: 'PO', color: '#888888', category: 'phosphate'},
|
|
124
|
+
'sp': {name: 'Phosphorothioate', short: 'PS', color: '#6B46C1', category: 'phosphate'},
|
|
125
|
+
's2p': {name: 'Phosphorodithioate', short: 'PS₂', color: '#7E2BC4', category: 'phosphate'},
|
|
126
|
+
'mp': {name: 'Methylphosphonate', short: 'MeP', color: '#9B5DE5', category: 'phosphate'},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
export const PHOSPHATE_ALIASES: Readonly<Record<string, string>> = Object.freeze({
|
|
130
|
+
'P': 'p',
|
|
131
|
+
'sP': 'sp',
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
export const CONJUGATE_MODS: Readonly<Record<string, ModMeta>> = Object.freeze({
|
|
135
|
+
'GalNAc': {name: 'GalNAc', short: 'GalNAc', color: '#E91E63', category: 'conjugate'},
|
|
136
|
+
'L3': {name: 'GalNAc-L3 linker', short: 'L3', color: '#E91E63', category: 'conjugate'},
|
|
137
|
+
'Chol': {name: 'Cholesterol', short: 'Chol', color: '#B57F50', category: 'conjugate'},
|
|
138
|
+
'Bio': {name: 'Biotin', short: 'Bio', color: '#FFC107', category: 'conjugate'},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/** Pale per-base colors used when sugar is unmodified (R / dR).
|
|
142
|
+
* Modifications override these. */
|
|
143
|
+
export const BASE_COLORS: Readonly<Record<string, string>> = Object.freeze({
|
|
144
|
+
A: '#C1E7C5', // pale green
|
|
145
|
+
C: '#C5DCF0', // pale blue
|
|
146
|
+
G: '#E8DCA9', // pale tan
|
|
147
|
+
U: '#F0C5C5', // pale pink
|
|
148
|
+
T: '#F0C5C5',
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
export const FALLBACK_COLOR = '#BCBCBC';
|
|
152
|
+
|
|
153
|
+
/** Deterministic color for unknown HELM monomer symbols. Stable across cells. */
|
|
154
|
+
export function hashColor(symbol: string): string {
|
|
155
|
+
let h = 0;
|
|
156
|
+
for (let i = 0; i < symbol.length; i++)
|
|
157
|
+
h = ((h * 31) + symbol.charCodeAt(i)) | 0;
|
|
158
|
+
const hue = ((h >>> 0) % 360);
|
|
159
|
+
return `hsl(${hue}, 42%, 60%)`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Canonicalize a sugar symbol via the alias map, then return the canonical form. */
|
|
163
|
+
export function canonicalSugarSymbol(sugar: string): string {
|
|
164
|
+
return SUGAR_ALIASES[sugar] ?? sugar;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Canonicalize a phosphate symbol via the alias map. */
|
|
168
|
+
export function canonicalPhosphateSymbol(phosphate: string): string {
|
|
169
|
+
return PHOSPHATE_ALIASES[phosphate] ?? phosphate;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Resolve a sugar HELM symbol to (color + meta). Accepts canonical or aliased input. */
|
|
173
|
+
export function resolveSugar(sugar: string, base: string | null): { color: string; meta: ModMeta } {
|
|
174
|
+
const canonical = canonicalSugarSymbol(sugar);
|
|
175
|
+
const known = SUGAR_MODS[canonical];
|
|
176
|
+
if (known) {
|
|
177
|
+
// For ribose / deoxyribose use the per-base canonical color rather than a flat gray
|
|
178
|
+
if ((canonical === 'r' || canonical === 'd') && base && BASE_COLORS[base])
|
|
179
|
+
return {color: BASE_COLORS[base], meta: known};
|
|
180
|
+
return {color: known.color, meta: known};
|
|
181
|
+
}
|
|
182
|
+
const c = hashColor(canonical);
|
|
183
|
+
return {color: c, meta: {name: canonical, short: canonical, color: c, category: 'sugar'}};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function resolvePhosphate(phosphate: string): { color: string; meta: ModMeta } {
|
|
187
|
+
if (!phosphate)
|
|
188
|
+
return {color: '#888', meta: {name: '(none)', short: '', color: '#888', category: 'phosphate'}};
|
|
189
|
+
const canonical = canonicalPhosphateSymbol(phosphate);
|
|
190
|
+
const known = PHOSPHATE_MODS[canonical];
|
|
191
|
+
if (known)
|
|
192
|
+
return {color: known.color, meta: known};
|
|
193
|
+
const c = hashColor(canonical);
|
|
194
|
+
return {color: c, meta: {name: canonical, short: canonical, color: c, category: 'phosphate'}};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function resolveConjugate(symbol: string): { color: string; meta: ModMeta } {
|
|
198
|
+
const known = CONJUGATE_MODS[symbol];
|
|
199
|
+
if (known)
|
|
200
|
+
return {color: known.color, meta: known};
|
|
201
|
+
return {
|
|
202
|
+
color: hashColor(symbol),
|
|
203
|
+
meta: {name: symbol, short: symbol.length > 6 ? symbol.slice(0, 6) : symbol, color: hashColor(symbol), category: 'conjugate'},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Pick black or white text for readability on a given background hex/rgb/hsl color. */
|
|
208
|
+
export function contrastTextColor(bg: string): string {
|
|
209
|
+
// Resolve any CSS color string to RGB through a temp DOM element
|
|
210
|
+
const probe = document.createElement('span');
|
|
211
|
+
probe.style.color = bg;
|
|
212
|
+
document.body.appendChild(probe);
|
|
213
|
+
const computed = getComputedStyle(probe).color;
|
|
214
|
+
document.body.removeChild(probe);
|
|
215
|
+
const m = computed.match(/\d+/g);
|
|
216
|
+
if (!m) return '#000';
|
|
217
|
+
const [r, g, b] = m.slice(0, 3).map(Number);
|
|
218
|
+
// perceptual luminance — black on light, white on dark
|
|
219
|
+
const lum = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
220
|
+
return lum > 160 ? '#1a1a1a' : '#ffffff';
|
|
221
|
+
}
|
package/src/package-api.ts
CHANGED
|
@@ -102,6 +102,13 @@ export namespace funcs {
|
|
|
102
102
|
return await grok.functions.call('SequenceTranslator:PolyToolEnumerateChemTopMenu', {});
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
Enumerate cores and R-group lists into a molecule table (Zip or Cartesian)
|
|
107
|
+
*/
|
|
108
|
+
export async function chemEnumerateReactionsTopMenu(): Promise<void> {
|
|
109
|
+
return await grok.functions.call('SequenceTranslator:ChemEnumerateReactionsTopMenu', {});
|
|
110
|
+
}
|
|
111
|
+
|
|
105
112
|
export async function polyToolColumnChoice(df: DG.DataFrame , macroMolecule: DG.Column ): Promise<void> {
|
|
106
113
|
return await grok.functions.call('SequenceTranslator:PolyToolColumnChoice', { df, macroMolecule });
|
|
107
114
|
}
|
|
@@ -114,7 +121,7 @@ export namespace funcs {
|
|
|
114
121
|
return await grok.functions.call('SequenceTranslator:PtEnumeratorHelmApp', {});
|
|
115
122
|
}
|
|
116
123
|
|
|
117
|
-
export async function ptEnumeratorChemApp(): Promise<
|
|
124
|
+
export async function ptEnumeratorChemApp(): Promise<DG.View> {
|
|
118
125
|
return await grok.functions.call('SequenceTranslator:PtEnumeratorChemApp', {});
|
|
119
126
|
}
|
|
120
127
|
|
|
@@ -144,6 +151,41 @@ export namespace funcs {
|
|
|
144
151
|
return await grok.functions.call('SequenceTranslator:GetPolyToolCombineDialog', {});
|
|
145
152
|
}
|
|
146
153
|
|
|
154
|
+
/**
|
|
155
|
+
Renders OligoNucleotide (siRNA / ASO) duplex view in grid cells
|
|
156
|
+
*/
|
|
157
|
+
export async function oligoNucleotideCellRenderer(): Promise<any> {
|
|
158
|
+
return await grok.functions.call('SequenceTranslator:OligoNucleotideCellRenderer', {});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
Modifications, lengths, conjugates and color legend for an OligoNucleotide cell
|
|
163
|
+
*/
|
|
164
|
+
export async function oligoNucleotidePanel(value: any ): Promise<any> {
|
|
165
|
+
return await grok.functions.call('SequenceTranslator:OligoNucleotidePanel', { value });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
Sense and antisense full molecular structures rendered separately
|
|
170
|
+
*/
|
|
171
|
+
export async function oligoNucleotideStructuresPanel(value: any ): Promise<any> {
|
|
172
|
+
return await grok.functions.call('SequenceTranslator:OligoNucleotideStructuresPanel', { value });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
Create a new column tagged as OligoNucleotide so HELM duplex cells render with the oligo view
|
|
177
|
+
*/
|
|
178
|
+
export async function convertHelmToOligoNucleotide(table: DG.DataFrame , helmCol: DG.Column ): Promise<DG.Column> {
|
|
179
|
+
return await grok.functions.call('SequenceTranslator:ConvertHelmToOligoNucleotide', { table, helmCol });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
Combine separate sense + antisense HELM columns into one OligoNucleotide column
|
|
184
|
+
*/
|
|
185
|
+
export async function combineSenseAntisenseToOligoNucleotide(table: DG.DataFrame , senseCol: DG.Column , antiCol: DG.Column ): Promise<DG.Column> {
|
|
186
|
+
return await grok.functions.call('SequenceTranslator:CombineSenseAntisenseToOligoNucleotide', { table, senseCol, antiCol });
|
|
187
|
+
}
|
|
188
|
+
|
|
147
189
|
export async function applyNotationProviderForCyclized(col: DG.Column , separator: string ): Promise<void> {
|
|
148
190
|
return await grok.functions.call('SequenceTranslator:ApplyNotationProviderForCyclized', { col, separator });
|
|
149
191
|
}
|
package/src/package-test.ts
CHANGED
|
@@ -13,9 +13,11 @@ import './tests/polytool-convert-tests';
|
|
|
13
13
|
import './tests/polytool-unrule-tests';
|
|
14
14
|
import './tests/polytool-enumerate-tests';
|
|
15
15
|
import './tests/polytool-enumerate-breadth-tests';
|
|
16
|
+
import './tests/polytool-enumerate-chem-tests';
|
|
16
17
|
import './tests/polytool-chain-parse-notation-tests';
|
|
17
18
|
import './tests/polytool-chain-from-notation-tests';
|
|
18
19
|
import './tests/toAtomicLevel-tests';
|
|
20
|
+
import './tests/oligo-renderer-tests';
|
|
19
21
|
|
|
20
22
|
import {OligoToolkitTestPackage} from './tests/utils';
|
|
21
23
|
|
package/src/package.g.ts
CHANGED
|
@@ -148,6 +148,13 @@ export async function polyToolEnumerateChemTopMenu() : Promise<void> {
|
|
|
148
148
|
await PackageFunctions.polyToolEnumerateChemTopMenu();
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
//name: chemEnumerateReactions
|
|
152
|
+
//description: Enumerate cores and R-group lists into a molecule table (Zip or Cartesian)
|
|
153
|
+
//top-menu: Chem | Transform | Reactions | Enumerate...
|
|
154
|
+
export async function chemEnumerateReactionsTopMenu() : Promise<void> {
|
|
155
|
+
await PackageFunctions.chemEnumerateReactionsTopMenu();
|
|
156
|
+
}
|
|
157
|
+
|
|
151
158
|
//input: dataframe df { description: Input data table }
|
|
152
159
|
//input: column macroMolecule
|
|
153
160
|
export async function polyToolColumnChoice(df: DG.DataFrame, macroMolecule: DG.Column) : Promise<void> {
|
|
@@ -170,11 +177,12 @@ export async function ptEnumeratorHelmApp() : Promise<void> {
|
|
|
170
177
|
|
|
171
178
|
//name: Chem Enumerator
|
|
172
179
|
//tags: app
|
|
180
|
+
//output: view result
|
|
173
181
|
//meta.icon: img/icons/structure.png
|
|
174
|
-
//meta.browsePath:
|
|
182
|
+
//meta.browsePath: Chem | PolyTool
|
|
175
183
|
//meta.role: app
|
|
176
|
-
export async function ptEnumeratorChemApp()
|
|
177
|
-
await PackageFunctions.ptEnumeratorChemApp();
|
|
184
|
+
export async function ptEnumeratorChemApp() {
|
|
185
|
+
return await PackageFunctions.ptEnumeratorChemApp();
|
|
178
186
|
}
|
|
179
187
|
|
|
180
188
|
//name: Polytool Helm Enumerator dialog
|
|
@@ -215,6 +223,51 @@ export async function getPolyToolCombineDialog() : Promise<void> {
|
|
|
215
223
|
await PackageFunctions.getPolyToolCombineDialog();
|
|
216
224
|
}
|
|
217
225
|
|
|
226
|
+
//description: Renders OligoNucleotide (siRNA / ASO) duplex view in grid cells
|
|
227
|
+
//tags: cellRenderer
|
|
228
|
+
//output: grid_cell_renderer result
|
|
229
|
+
//meta.cellType: OligoNucleotide
|
|
230
|
+
//meta.columnTags: quality=OligoNucleotide
|
|
231
|
+
//meta.role: cellRenderer
|
|
232
|
+
export function oligoNucleotideCellRenderer() : any {
|
|
233
|
+
return PackageFunctions.oligoNucleotideCellRenderer();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
//name: Oligo-Nucleotide
|
|
237
|
+
//description: Modifications, lengths, conjugates and color legend for an OligoNucleotide cell
|
|
238
|
+
//tags: panel, widgets
|
|
239
|
+
//input: semantic_value value { semType: OligoNucleotide }
|
|
240
|
+
//output: widget result
|
|
241
|
+
export function oligoNucleotidePanel(value: DG.SemanticValue) : any {
|
|
242
|
+
return PackageFunctions.oligoNucleotidePanel(value);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
//name: Oligo Structures
|
|
246
|
+
//description: Sense and antisense full molecular structures rendered separately
|
|
247
|
+
//tags: panel, widgets
|
|
248
|
+
//input: semantic_value value { semType: OligoNucleotide }
|
|
249
|
+
//output: widget result
|
|
250
|
+
export function oligoNucleotideStructuresPanel(value: DG.SemanticValue) : any {
|
|
251
|
+
return PackageFunctions.oligoNucleotideStructuresPanel(value);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
//description: Create a new column tagged as OligoNucleotide so HELM duplex cells render with the oligo view
|
|
255
|
+
//input: dataframe table
|
|
256
|
+
//input: column helmCol { caption: HELM column; semType: Macromolecule }
|
|
257
|
+
//output: column result
|
|
258
|
+
export async function convertHelmToOligoNucleotide(table: DG.DataFrame, helmCol: DG.Column) : Promise<any> {
|
|
259
|
+
return await PackageFunctions.convertHelmToOligoNucleotide(table, helmCol);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
//description: Combine separate sense + antisense HELM columns into one OligoNucleotide column
|
|
263
|
+
//input: dataframe table
|
|
264
|
+
//input: column senseCol { caption: Sense; semType: Macromolecule }
|
|
265
|
+
//input: column antiCol { caption: Antisense; semType: Macromolecule }
|
|
266
|
+
//output: column result
|
|
267
|
+
export async function combineSenseAntisenseToOligoNucleotide(table: DG.DataFrame, senseCol: DG.Column, antiCol: DG.Column) : Promise<any> {
|
|
268
|
+
return await PackageFunctions.combineSenseAntisenseToOligoNucleotide(table, senseCol, antiCol);
|
|
269
|
+
}
|
|
270
|
+
|
|
218
271
|
//name: applyNotationProviderForHarmonizedSequence
|
|
219
272
|
//input: column col
|
|
220
273
|
//input: string separator
|