Skip to content

Commit 5d42f6d

Browse files
committed
Update playground to show all molecules on load, add transitional comments
Playground now renders everything in a grid on load, grouped by category. Added heterocycles, more drugs, drug-like compounds from the test set, and a click-to-enlarge detail view. Clarified the comment on _shouldInvertStereoParity in plain english.
1 parent b19e798 commit 5d42f6d

3 files changed

Lines changed: 199 additions & 91 deletions

File tree

dist/smiles-drawer.js

Lines changed: 11 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/DrawerBase.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3147,12 +3147,15 @@ export default class DrawerBase {
31473147
/**
31483148
* TRANSITIONAL — do not add more patterns here without good reason.
31493149
*
3150-
* This flips the R/S parity for specific parser-order edge cases where
3151-
* the neighbor ordering we get from the SMILES parser disagrees with
3152-
* what RDKit expects. It works for most molecules but is brittle:
3153-
* each pattern was matched empirically against the RDKit reference set.
3154-
* The long-term fix is a single canonical parity model that does not
3155-
* depend on parser traversal order at all.
3150+
* The R/S assignment depends on the order we visit neighbors, but
3151+
* the SMILES parser doesn't always give them in the right order.
3152+
* When that happens, we get R instead of S or vice versa. This
3153+
* method catches those specific cases and flips the result.
3154+
*
3155+
* It works, but it's fragile — each pattern was found by trial and
3156+
* error, not derived from first principles. The real fix is to stop
3157+
* depending on parse order entirely and use a single canonical way
3158+
* to assign parity.
31563159
*
31573160
* @param {Vertex} vertex The stereocenter.
31583161
* @param {Number[]} neighbours Neighbors in current local order.

test/playground.html

Lines changed: 179 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -14,112 +14,160 @@
1414
cursor: pointer; background: #2563eb; color: white; }
1515
button:hover { background: #1d4ed8; }
1616
#theme-toggle { background: #555; }
17-
.grid { display: flex; flex-wrap: wrap; gap: 12px; }
18-
.mol-card { background: white; border-radius: 6px; padding: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
17+
.section-title { font-size: 14px; font-weight: 600; color: #374151; margin: 20px 0 8px 0;
18+
border-bottom: 1px solid #ccc; padding-bottom: 4px; width: 100%; }
19+
.section-title:first-child { margin-top: 0; }
20+
.grid { display: flex; flex-wrap: wrap; gap: 10px; }
21+
.mol-card { background: white; border-radius: 6px; padding: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);
22+
cursor: pointer; transition: box-shadow 0.15s; }
23+
.mol-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.2); }
1924
.mol-card svg { display: block; }
20-
.mol-label { font-size: 11px; color: #666; font-family: monospace; margin-top: 4px;
21-
max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
25+
.mol-label { font-size: 10px; color: #666; font-family: monospace; margin-top: 3px;
26+
max-width: 250px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2227
.error { color: #dc2626; font-size: 12px; margin-top: 4px; }
23-
#presets { margin-bottom: 16px; display: flex; flex-wrap: wrap; gap: 6px; }
24-
.preset { padding: 4px 10px; font-size: 12px; background: #e5e7eb; border: none;
25-
border-radius: 12px; cursor: pointer; }
26-
.preset:hover { background: #d1d5db; }
28+
#detail-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0;
29+
background: rgba(0,0,0,0.5); z-index: 100; justify-content: center; align-items: center; }
30+
#detail-overlay.visible { display: flex; }
31+
#detail-card { background: white; border-radius: 8px; padding: 20px; max-width: 90vw; max-height: 90vh; }
32+
#detail-card svg { display: block; margin: 0 auto; }
33+
#detail-smiles { font-family: monospace; font-size: 13px; color: #555; margin-top: 10px;
34+
word-break: break-all; max-width: 600px; text-align: center; }
2735
</style>
2836
</head>
2937
<body>
3038
<h1>SmilesDrawer Playground</h1>
3139
<div class="controls">
32-
<input id="smiles-input" type="text" placeholder="Enter SMILES string..." value="CC1C(=O)NCCN1C(=O)C1(CNC(=O)[C@@H]2[C@H]3CCOC[C@H]32)CC=CC1">
33-
<button onclick="drawSmiles()">Draw</button>
40+
<input id="smiles-input" type="text" placeholder="Enter SMILES string...">
41+
<button onclick="drawFromInput()">Draw</button>
3442
<button id="theme-toggle" onclick="toggleTheme()">Theme: light</button>
35-
<button onclick="drawAll()">Draw All Presets</button>
3643
</div>
37-
<div id="presets"></div>
3844
<div id="output" class="grid"></div>
3945

46+
<div id="detail-overlay" onclick="closeDetail(event)">
47+
<div id="detail-card">
48+
<div id="detail-svg"></div>
49+
<div id="detail-smiles"></div>
50+
</div>
51+
</div>
52+
4053
<script src="../dist/smiles-drawer.js"></script>
4154
<script>
4255
let currentTheme = 'light';
4356

4457
const presets = {
45-
// The molecule from the bug report
46-
'oxanorbornane (bug)': 'CC1C(=O)NCCN1C(=O)C1(CNC(=O)[C@@H]2[C@H]3CCOC[C@H]32)CC=CC1',
47-
// Simple molecules
48-
'methanol': 'CO',
49-
'ethane': 'CC',
50-
'acetic acid': 'CC(=O)O',
51-
// Rings
52-
'benzene': 'c1ccccc1',
53-
'naphthalene': 'c1ccc2ccccc2c1',
54-
'cyclohexane': 'C1CCCCC1',
55-
// Bridged rings
56-
'norbornane': 'C1CC2CC1CC2',
57-
'camphor': 'CC1(C)C2CCC1(C)C(=O)C2',
58-
// Fused rings
59-
'indole': 'c1ccc2[nH]ccc2c1',
60-
'anthracene': 'c1ccc2cc3ccccc3cc2c1',
61-
// Drugs
62-
'aspirin': 'CC(=O)Oc1ccccc1C(=O)O',
63-
'caffeine': 'Cn1cnc2c1c(=O)n(c(=O)n2C)C',
64-
'ibuprofen': 'CC(C)Cc1ccc(cc1)C(C)C(=O)O',
65-
'nicotine': 'CN1CCC[C@H]1c1cccnc1',
66-
'penicillin G': 'CC1([C@@H](N2[C@H](S1)[C@@H](C2=O)NC(=O)Cc1ccccc1)C(=O)O)C',
67-
// Issue molecules
68-
'#162 polyphenyl': 'C(C1=CC=CC=C1)1=CC=C(C2C=CC(C3C=CC(C4C=CC(C5C=CC=CC=5)=CC=4)=CC=3)=CC=2)C=C1',
69-
'#209 pseudo charge': 'CCS(=O)(=O)[O-]',
70-
'#217 stereo': 'C/C(=C\\C(=O)OC)/C1=CC=C(C=C1)C(C)(F)F',
71-
'#188 fused rings': 'O(C(c1cccc2c1cccc2)c3cccc4ccccc34)C(C(C)Sc5ccc(cc5)Br)=O',
72-
// Charged / salts
73-
'NaCl': '[Na+].[Cl-]',
74-
'[NH4]+': '[NH4+]',
75-
// Stereo
76-
'L-alanine': 'N[C@@H](C)C(=O)O',
77-
'cholesterol': 'C([C@@H]1CC2=CC(O)CC[C@@]2(C)[C@H]1[C@H]1CC[C@@]2([C@@H]1CC1=C)[C@H](C)CC[C@H]2C)C',
78-
// Nucleotides / complex stereo
79-
'NAD+': 'O=C(N)c1ccc[n+](c1)[C@H]2[C@H](O)[C@H](O)[C@H](O2)COP([O-])(=O)OP(=O)(O)OC[C@H]3O[C@@H](n4cnc5c4ncnc5N)[C@@H]([C@@H]3O)OP(=O)(O)O',
80-
'adenosine': 'n2c1c(ncnc1n(c2)[C@@H]3O[C@@H]([C@@H](O)[C@H]3O)CO)N',
81-
'AZT-like': 'O=C1NC(C(C)=CN1[C@@H]2O[C@H](CO)[C@@H](N=[N+]=[N-])C2)=O',
82-
// Complex
83-
'morphine': 'CN1CC[C@]23[C@@H]4[C@H]1CC5=C2C(=C(C=C5)O)O[C@H]3[C@H](C=C4)O',
84-
'glucose': 'OC[C@H]1OC(O)[C@H](O)[C@@H](O)[C@@H]1O',
85-
'vancomycin partial': 'CC(O)C(NC(=O)C1CC(=O)N1)C(=O)NCC(=O)O',
58+
'Simple': {
59+
'methanol': 'CO',
60+
'ethane': 'CC',
61+
'acetic acid': 'CC(=O)O',
62+
'benzene': 'c1ccccc1',
63+
'cyclohexane': 'C1CCCCC1',
64+
'cyclopropane': 'C1CC1',
65+
'ethene': 'C=C',
66+
'ethyne': 'C#C',
67+
},
68+
'E/Z double bonds': {
69+
'trans-difluoroethene (E)': 'F/C=C/F',
70+
'cis-difluoroethene (Z)': 'F/C=C\\F',
71+
'trans-dichloroethene (E)': 'Cl/C=C/Cl',
72+
'cis-dichloroethene (Z)': 'Cl/C=C\\Cl',
73+
'trans-2-butene (E)': 'C/C=C/C',
74+
'cis-2-butene (Z)': 'C/C=C\\C',
75+
'#217 branched E/Z': 'C/C(=C\\C(=O)OC)/C1=CC=C(C=C1)C(C)(F)F',
76+
'ring-connected E/Z': 'C/C=C/1\\CCC[C@@]2(C1CC2)C',
77+
'CID 153456993': 'C1=CC(=CC=C1CS)/C=C\\2/C(=O)N(C(=O)S2)CC(=O)O',
78+
'CID 102485436': 'CN(C)CCCN/C(=C\\C(=O)C1=CC=CC=C1)/C2=CC=CC=C2',
79+
'CID 173497121': 'CC(/C(=C(/C(C)OC1=CC=C(C=C1)O)\\O)/C(O)OC)O',
80+
'tamoxifen': 'CCC(=C(C1=CC=CC=C1)C2=CC=C(C=C2)OCCN(C)C)C3=CC=CC=C3',
81+
},
82+
'Stereo R/S': {
83+
'L-alanine': 'N[C@@H](C)C(=O)O',
84+
'D-alanine': 'N[C@H](C)C(=O)O',
85+
'nicotine': 'CN1CCC[C@H]1c1cccnc1',
86+
'penicillin G': 'CC1([C@@H](N2[C@H](S1)[C@@H](C2=O)NC(=O)Cc1ccccc1)C(=O)O)C',
87+
'glucose': 'OC[C@H]1OC(O)[C@H](O)[C@@H](O)[C@@H]1O',
88+
'cholesterol': 'C([C@@H]1CC2=CC(O)CC[C@@]2(C)[C@H]1[C@H]1CC[C@@]2([C@@H]1CC1=C)[C@H](C)CC[C@H]2C)C',
89+
},
90+
'Bridged rings': {
91+
'norbornane': 'C1CC2CC1CC2',
92+
'norbornane variant': 'C1CC2CCC1C2',
93+
'camphor': 'CC1(C)C2CCC1(C)C(=O)C2',
94+
'adamantane': 'C1C2CC3CC1CC(C2)C3',
95+
'alpha-pinene': 'CC1=CCC2CC1C2(C)C',
96+
},
97+
'Fused rings': {
98+
'naphthalene': 'c1ccc2ccccc2c1',
99+
'anthracene': 'c1ccc2cc3ccccc3cc2c1',
100+
'indole': 'c1ccc2[nH]ccc2c1',
101+
'quinoline': 'c1ccc2ncccc2c1',
102+
'phenanthrene': 'c1ccc2c(c1)ccc1ccccc12',
103+
'fluorene': 'c1ccc2c(c1)Cc1ccccc1-2',
104+
'#188 polycyclic': 'O(C(c1cccc2c1cccc2)c3cccc4ccccc34)C(C(C)Sc5ccc(cc5)Br)=O',
105+
},
106+
'Drugs': {
107+
'aspirin': 'CC(=O)Oc1ccccc1C(=O)O',
108+
'caffeine': 'Cn1cnc2c1c(=O)n(c(=O)n2C)C',
109+
'ibuprofen': 'CC(C)Cc1ccc(cc1)C(C)C(=O)O',
110+
'morphine': 'CN1CC[C@]23[C@@H]4[C@H]1CC5=C2C(=C(C=C5)O)O[C@H]3[C@H](C=C4)O',
111+
'codeine': 'COC1=CC2=C(C=C1)[C@@H]3CC=C[C@H]4[C@@H]5N(CC[C@]34[C@@H]2O5)C',
112+
'taxol': 'CC1=C2[C@@]([C@]([C@H]([C@@H]3[C@]4([C@H](OC4)C[C@@H]([C@]3(C(=O)[C@@H]2OC(=O)C)C)O)OC(=O)C)OC(=O)c5ccccc5)(C[C@@H]1OC(=O)[C@@H](O)[C@@H](NC(=O)c6ccccc6)c7ccccc7)O)(C)C',
113+
'simvastatin': 'CCC(C)(C)C(=O)O[C@H]1C[C@@H](O)C=C2C=C[C@H](C)[C@H](CC[C@@H](O)CC(=O)O)[C@@H]12',
114+
'lovastatin': 'CC[C@H](C)C(=O)O[C@H]1C[C@@H](O)C=C2C=C[C@H](C)[C@H](CC[C@@H](O)CC(=O)O)[C@@H]12',
115+
'warfarin': 'CC(=O)CC(C1=CC=CC=C1)C2=C(O)C3=CC=CC=C3OC2=O',
116+
'metformin': 'CN(C)C(=N)NC(=N)N',
117+
'sildenafil': 'CCCC1=NN(C)C2=C1NC(=NC2=O)C1=CC(=CC=C1OCC)S(=O)(=O)N1CCN(C)CC1',
118+
'omeprazole': 'CC1=CN=C(C(=C1OC)C)CS(=O)C2=NC3=CC=CC=C3N2',
119+
},
120+
'Charged / salts': {
121+
'NaCl': '[Na+].[Cl-]',
122+
'[NH4]+': '[NH4+]',
123+
'#209 pseudo charge': 'CCS(=O)(=O)[O-]',
124+
'glycine zwitterion': '[NH3+]CC([O-])=O',
125+
'calcium acetate': '[Ca+2].[O-]C(=O)C.[O-]C(=O)C',
126+
},
127+
'Heterocycles': {
128+
'pyridine': 'c1ccncc1',
129+
'pyrimidine': 'c1ccnc(n1)',
130+
'thiophene': 'c1ccsc1',
131+
'furan': 'c1ccoc1',
132+
'imidazole': 'c1cnc[nH]1',
133+
'purine': 'c1ncc2[nH]cnc2n1',
134+
},
135+
'Drug-like (from test set)': {
136+
'thiazoline-A': 'CC1=CC(C)=CC(N=C2SC=C(C3=CC=C(Br)O3)N2CC2CN(S(C)(=O)=O)C2)=C1',
137+
'thiazoline-B': 'COC(=O)C1=CC(C2=CSC(=NC3=C(C)C=CC=C3C)N2C[C@H](C)OC)=C(C)O1',
138+
'thiazoline-C': 'CC1=CC=CC(C)=C1N=C1SC=C(C2=CC=NN2C)N1CCC2COC(C)(C)O2',
139+
'thiazoline-D': 'O=C1NC[C@H](CN2C(C3=CC=C([N+](=O)[O-])O3)=CSC2=NC2=CC=CC=C2F)O1',
140+
'thiazoline-E': 'COC1=CC=C(C2=CSC(=NC3=CC=CC=C3F)N2C[C@H]2CCC(=O)N2)C=C1O',
141+
'thiazoline-F': 'CN1C=C(C2=CSC(=NC3=CC=C(F)C(Cl)=C3)N2CC[C@H]2C[C@H]3C=C[C@@H]2C3)N=N1',
142+
},
143+
'Complex / stress test': {
144+
'#162 polyphenyl': 'C(C1=CC=CC=C1)1=CC=C(C2C=CC(C3C=CC(C4C=CC(C5C=CC=CC=5)=CC=4)=CC=3)=CC=2)C=C1',
145+
'NAD+': 'O=C(N)c1ccc[n+](c1)[C@H]2[C@H](O)[C@H](O)[C@H](O2)COP([O-])(=O)OP(=O)(O)OC[C@H]3O[C@@H](n4cnc5c4ncnc5N)[C@@H]([C@@H]3O)OP(=O)(O)O',
146+
'adenosine': 'n2c1c(ncnc1n(c2)[C@@H]3O[C@@H]([C@@H](O)[C@H]3O)CO)N',
147+
'vancomycin partial': 'CC(O)C(NC(=O)C1CC(=O)N1)C(=O)NCC(=O)O',
148+
'oxanorbornane (bug)': 'CC1C(=O)NCCN1C(=O)C1(CNC(=O)[C@@H]2[C@H]3CCOC[C@H]32)CC=CC1',
149+
'AZT-like': 'O=C1NC(C(C)=CN1[C@@H]2O[C@H](CO)[C@@H](N=[N+]=[N-])C2)=O',
150+
},
86151
};
87152

88-
// Build preset buttons
89-
const presetsDiv = document.getElementById('presets');
90-
for (const [name, smiles] of Object.entries(presets)) {
91-
const btn = document.createElement('button');
92-
btn.className = 'preset';
93-
btn.textContent = name;
94-
btn.onclick = () => {
95-
document.getElementById('smiles-input').value = smiles;
96-
drawSmiles();
97-
};
98-
presetsDiv.appendChild(btn);
99-
}
100-
101153
function toggleTheme() {
102154
currentTheme = currentTheme === 'light' ? 'dark' : 'light';
103155
document.getElementById('theme-toggle').textContent = 'Theme: ' + currentTheme;
104-
if (currentTheme === 'dark') {
105-
document.body.style.background = '#1a1a1a';
106-
document.body.style.color = '#eee';
107-
} else {
108-
document.body.style.background = '#f5f5f5';
109-
document.body.style.color = '#000';
110-
}
156+
document.body.style.background = currentTheme === 'dark' ? '#1a1a1a' : '#f5f5f5';
157+
document.body.style.color = currentTheme === 'dark' ? '#eee' : '#000';
158+
drawAll();
111159
}
112160

113-
function drawSingle(smiles, label) {
161+
function drawMolecule(smiles, label, size) {
114162
const card = document.createElement('div');
115163
card.className = 'mol-card';
116164
if (currentTheme === 'dark') card.style.background = '#2a2a2a';
117165

118166
try {
119-
const drawer = new SmilesDrawer.SmiDrawer({ width: 300, height: 300 });
167+
const drawer = new SmilesDrawer.SmiDrawer({ width: size, height: size });
120168
const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
121-
svgEl.setAttribute('width', '300');
122-
svgEl.setAttribute('height', '300');
169+
svgEl.setAttribute('width', size);
170+
svgEl.setAttribute('height', size);
123171
card.appendChild(svgEl);
124172
drawer.draw(smiles, svgEl, currentTheme);
125173
} catch (e) {
@@ -135,27 +183,75 @@ <h1>SmilesDrawer Playground</h1>
135183
lbl.title = smiles;
136184
card.appendChild(lbl);
137185

186+
card.onclick = () => showDetail(smiles, label);
138187
return card;
139188
}
140189

141-
function drawSmiles() {
190+
function showDetail(smiles, label) {
191+
const container = document.getElementById('detail-svg');
192+
const smilesLabel = document.getElementById('detail-smiles');
193+
const card = document.getElementById('detail-card');
194+
container.innerHTML = '';
195+
196+
if (currentTheme === 'dark') {
197+
card.style.background = '#2a2a2a';
198+
card.style.color = '#eee';
199+
} else {
200+
card.style.background = 'white';
201+
card.style.color = '#000';
202+
}
203+
204+
try {
205+
const drawer = new SmilesDrawer.SmiDrawer({ width: 600, height: 600 });
206+
const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
207+
svgEl.setAttribute('width', '600');
208+
svgEl.setAttribute('height', '600');
209+
container.appendChild(svgEl);
210+
drawer.draw(smiles, svgEl, currentTheme);
211+
} catch (e) {
212+
container.innerHTML = '<div class="error">Error: ' + e.message + '</div>';
213+
}
214+
215+
smilesLabel.textContent = (label ? label + ': ' : '') + smiles;
216+
document.getElementById('detail-overlay').classList.add('visible');
217+
}
218+
219+
function closeDetail(event) {
220+
if (event.target === document.getElementById('detail-overlay')) {
221+
document.getElementById('detail-overlay').classList.remove('visible');
222+
}
223+
}
224+
225+
document.addEventListener('keydown', (e) => {
226+
if (e.key === 'Escape') {
227+
document.getElementById('detail-overlay').classList.remove('visible');
228+
}
229+
});
230+
231+
function drawFromInput() {
142232
const smiles = document.getElementById('smiles-input').value.trim();
143233
if (!smiles) return;
144234
const output = document.getElementById('output');
145235
output.innerHTML = '';
146-
output.appendChild(drawSingle(smiles, smiles));
236+
output.appendChild(drawMolecule(smiles, smiles, 350));
147237
}
148238

149239
function drawAll() {
150240
const output = document.getElementById('output');
151241
output.innerHTML = '';
152-
for (const [name, smiles] of Object.entries(presets)) {
153-
output.appendChild(drawSingle(smiles, name + ': ' + smiles));
242+
for (const [category, molecules] of Object.entries(presets)) {
243+
const title = document.createElement('div');
244+
title.className = 'section-title';
245+
title.textContent = category;
246+
output.appendChild(title);
247+
for (const [name, smiles] of Object.entries(molecules)) {
248+
output.appendChild(drawMolecule(smiles, name, 250));
249+
}
154250
}
155251
}
156252

157-
// Draw initial molecule
158-
drawSmiles();
253+
// Draw everything on load
254+
drawAll();
159255
</script>
160256
</body>
161257
</html>

0 commit comments

Comments
 (0)