UNPKG

smiles-drawer

Version:

A SMILES drawer and parser. Generate molecular structure depictions in pure JavaScript.

475 lines (397 loc) 16.7 kB
(function(window) { var SVG_NS = 'http://www.w3.org/2000/svg'; var DEFAULT_RENDER_OPTIONS = { scale: 0, width: 550, height: 450, bondThickness: 1.1, bondLength: 19, shortBondLength: 0.6, bondSpacing: 3.2, fontSizeLarge: 6.3, fontSizeSmall: 2.4, padding: 10 }; function isArray(value) { return Object.prototype.toString.call(value) === '[object Array]'; } function isPlainObject(value) { return Object.prototype.toString.call(value) === '[object Object]'; } function clone(value) { var result; var key; var i; if (isArray(value)) { result = []; for (i = 0; i < value.length; i++) { result.push(clone(value[i])); } return result; } if (isPlainObject(value)) { result = {}; for (key in value) { if (Object.prototype.hasOwnProperty.call(value, key)) { result[key] = clone(value[key]); } } return result; } return value; } function mergeInto(target, source) { var key; if (!isPlainObject(source)) { return target; } for (key in source) { if (!Object.prototype.hasOwnProperty.call(source, key)) continue; if (isPlainObject(source[key]) && isPlainObject(target[key])) { mergeInto(target[key], source[key]); } else if (isPlainObject(source[key])) { target[key] = clone(source[key]); } else if (isArray(source[key])) { target[key] = clone(source[key]); } else { target[key] = source[key]; } } return target; } function mergeObjects() { var result = {}; var i; for (i = 0; i < arguments.length; i++) { mergeInto(result, arguments[i]); } return result; } function clamp(value, min, max) { return Math.min(max, Math.max(min, value)); } function round(value, decimals) { var factor = Math.pow(10, decimals || 0); return Math.round(value * factor) / factor; } function ensureOption(options, key, fallback) { if (typeof options[key] === 'undefined' || options[key] === null || options[key] === '') { options[key] = fallback; } } function getDefaultRenderOptions() { return clone(DEFAULT_RENDER_OPTIONS); } function waitForFonts(callback) { if (!callback) return; if (document.fonts && document.fonts.ready) { document.fonts.ready.then(function() { window.requestAnimationFrame(callback); }, function() { window.requestAnimationFrame(callback); }); return; } window.addEventListener('load', function() { window.requestAnimationFrame(callback); }, {once: true}); } function getPageTheme() { return document.documentElement.classList.contains('dark') ? 'dark' : 'light'; } function isReaction(smiles) { var depth = 0; var i; for (i = 0; i < smiles.length; i++) { if (smiles[i] === '[') depth++; else if (smiles[i] === ']') depth--; else if (smiles[i] === '>' && depth === 0) return true; } return false; } function estimateComplexity(smiles) { var ringMarkers = (smiles.match(/%\d{2}|\d/g) || []).length; var branches = (smiles.match(/[()]/g) || []).length; var stereo = (smiles.match(/@/g) || []).length; var hetero = (smiles.match(/Br|Cl|Si|Na|Li|Ca|Mg|[NOSPFIB]/g) || []).length; var charges = (smiles.match(/[+-]/g) || []).length; var bonds = (smiles.match(/[=#]/g) || []).length; return { length: smiles.length, ringMarkers: ringMarkers, branches: branches, stereo: stereo, hetero: hetero, charges: charges, bonds: bonds, score: smiles.length + ringMarkers * 4 + branches * 2 + stereo * 5 + hetero * 2 + charges * 3 + bonds }; } function getMoleculeFactor(score) { if (score >= 360) return 0.34; if (score >= 240) return 0.44; if (score >= 180) return 0.56; if (score >= 140) return 0.64; if (score >= 100) return 0.76; if (score >= 70) return 0.86; if (score >= 45) return 0.94; return 1; } function getReactionFactor(score) { if (score >= 180) return 0.66; if (score >= 130) return 0.76; if (score >= 95) return 0.86; return 1; } function buildMoleculeOptions(baseOptions, smiles, settings) { var options = mergeObjects(getDefaultRenderOptions(), baseOptions || {}); var adaptive = !settings || settings.adaptive !== false; var metrics = estimateComplexity(smiles); var factor = getMoleculeFactor(metrics.score); ensureOption(options, 'scale', DEFAULT_RENDER_OPTIONS.scale); ensureOption(options, 'bondThickness', DEFAULT_RENDER_OPTIONS.bondThickness); ensureOption(options, 'bondLength', DEFAULT_RENDER_OPTIONS.bondLength); ensureOption(options, 'shortBondLength', DEFAULT_RENDER_OPTIONS.shortBondLength); ensureOption(options, 'bondSpacing', DEFAULT_RENDER_OPTIONS.bondSpacing); ensureOption(options, 'fontSizeLarge', DEFAULT_RENDER_OPTIONS.fontSizeLarge); ensureOption(options, 'fontSizeSmall', DEFAULT_RENDER_OPTIONS.fontSizeSmall); ensureOption(options, 'padding', DEFAULT_RENDER_OPTIONS.padding); if (typeof options.isomeric === 'undefined') options.isomeric = true; if (typeof options.explicitHydrogens === 'undefined') options.explicitHydrogens = false; if (adaptive) { options.bondLength = round(clamp(options.bondLength * clamp(factor + 0.12, 0.46, 1), 7.2, DEFAULT_RENDER_OPTIONS.bondLength), 1); options.fontSizeLarge = round(clamp(options.fontSizeLarge * factor, 4.1, DEFAULT_RENDER_OPTIONS.fontSizeLarge), 1); options.fontSizeSmall = round(clamp(options.fontSizeSmall * clamp(factor + 0.08, 0.7, 1), 1.7, DEFAULT_RENDER_OPTIONS.fontSizeSmall), 1); options.padding = Math.round(clamp(options.padding * clamp(factor + 0.18, 0.7, 1), 6, 14)); if (typeof options.compactDrawing === 'undefined') { options.compactDrawing = metrics.score >= 70; } } else if (typeof options.compactDrawing === 'undefined') { options.compactDrawing = true; } if (settings && settings.renderConfig) { options = mergeObjects(options, settings.renderConfig); } return { metrics: metrics, options: options }; } function buildReactionOptions(baseOptions, smiles, settings) { var options = mergeObjects(getDefaultRenderOptions(), baseOptions || {}); var adaptive = !settings || settings.adaptive !== false; var metrics = estimateComplexity(smiles); var factor = getReactionFactor(metrics.score); ensureOption(options, 'scale', DEFAULT_RENDER_OPTIONS.scale); ensureOption(options, 'bondThickness', DEFAULT_RENDER_OPTIONS.bondThickness); ensureOption(options, 'bondLength', DEFAULT_RENDER_OPTIONS.bondLength); ensureOption(options, 'shortBondLength', DEFAULT_RENDER_OPTIONS.shortBondLength); ensureOption(options, 'bondSpacing', DEFAULT_RENDER_OPTIONS.bondSpacing); ensureOption(options, 'fontSizeLarge', DEFAULT_RENDER_OPTIONS.fontSizeLarge); ensureOption(options, 'fontSizeSmall', DEFAULT_RENDER_OPTIONS.fontSizeSmall); ensureOption(options, 'padding', DEFAULT_RENDER_OPTIONS.padding); if (typeof options.explicitHydrogens === 'undefined') options.explicitHydrogens = false; if (adaptive) { options.bondLength = round(clamp(options.bondLength * factor, 10.5, DEFAULT_RENDER_OPTIONS.bondLength), 1); options.fontSizeLarge = round(clamp(options.fontSizeLarge * clamp(factor + 0.04, 0.72, 1), 4.8, DEFAULT_RENDER_OPTIONS.fontSizeLarge), 1); options.padding = Math.round(clamp(options.padding * clamp(factor + 0.1, 0.78, 1), 7, 16)); } options.compactDrawing = false; if (settings && settings.renderConfig) { options = mergeObjects(options, settings.renderConfig); } return { metrics: metrics, options: options }; } function readJsonAttribute(node, attribute) { var value; if (!node) return null; value = node.getAttribute(attribute); if (!value) return null; try { return JSON.parse(value); } catch (error) { return null; } } function getRenderConfig(node) { return readJsonAttribute(node, 'data-render-config'); } function resetSvg(svg) { var fresh; var i; var attr; if (!svg) return svg; fresh = document.createElementNS(SVG_NS, 'svg'); for (i = 0; i < svg.attributes.length; i++) { attr = svg.attributes[i]; fresh.setAttribute(attr.name, attr.value); } if (svg.parentNode) { svg.parentNode.replaceChild(fresh, svg); } return fresh; } function applySvgBox(svg, kind, options, layout) { var config = layout || {}; var containerWidth = config.containerWidth || 0; var width = options && options.width ? options.width : 500; var height = options && options.height ? options.height : 500; if (!svg) return; svg.classList.add('w-full'); svg.style.width = '100%'; svg.style.height = 'auto'; svg.style.display = 'block'; svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); if (kind === 'reaction') { svg.style.maxWidth = '100%'; svg.style.maxHeight = Math.max(height, config.minHeight || 0) + 'px'; return; } svg.style.maxWidth = containerWidth > 0 ? Math.min(width, containerWidth) + 'px' : width + 'px'; svg.style.maxHeight = Math.max(height, config.minHeight || 0) + 'px'; } function applyThemeSurface(surface, themeName, themePresets, themeSurfaceModes) { var preset; var isDarkSurface; if (!surface || !themePresets) return; preset = themePresets[themeName] || themePresets.light; isDarkSurface = themeSurfaceModes && themeSurfaceModes[themeName] === 'dark'; surface.style.backgroundColor = preset.BACKGROUND || ''; surface.style.backgroundImage = isDarkSurface ? 'linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0))' : 'linear-gradient(180deg, rgba(255,255,255,0.72), rgba(255,255,255,0.2))'; } function renderSmiles(config) { var svg = config && config.svg; var smiles = config && config.smiles ? String(config.smiles).trim() : ''; var kind = config && config.kind && config.kind !== 'auto' ? config.kind : (isReaction(smiles) ? 'reaction' : 'molecule'); var profile = kind === 'reaction' ? buildReactionOptions(config && config.baseOptions ? config.baseOptions : {}, smiles, { adaptive: !config || config.adaptive !== false, renderConfig: config && config.renderConfig ? config.renderConfig : null }) : buildMoleculeOptions(config && config.baseOptions ? config.baseOptions : {}, smiles, { adaptive: !config || config.adaptive !== false, renderConfig: config && config.renderConfig ? config.renderConfig : null }); var themeName = config && config.themeName ? config.themeName : 'light'; function done(extra) { applySvgBox(svg, kind, profile.options, config && config.layout ? config.layout : null); if (config && config.onSuccess) { config.onSuccess(mergeObjects({ kind: kind, metrics: profile.metrics, options: profile.options, svg: svg }, extra || {})); } } function fail(error) { if (config && config.onError) { config.onError(error, kind, svg); } } if (!svg) { return { kind: kind, metrics: profile.metrics, options: profile.options, svg: svg }; } if (config && config.reset !== false) { svg = resetSvg(svg); } if (config && config.svgId) { svg.id = config.svgId; } applySvgBox(svg, kind, profile.options, config && config.layout ? config.layout : null); if (!smiles) { done({empty: true}); return { kind: kind, metrics: profile.metrics, options: profile.options, svg: svg }; } // Library ratio is sigma = bondLength / 3 (defaults: sigma 10, bondLength 30). // Mirror that ratio so custom bond lengths don't produce oversized heat blobs. var autoSigma = profile.options && profile.options.bondLength ? profile.options.bondLength / 3 : 10; try { if (kind === 'reaction') { new SmiDrawer( mergeObjects({}, profile.options, config && config.reactionWeights ? { weights: mergeObjects({ sigma: autoSigma, opacity: 0.72, additionalPadding: 30 }, config.weightOptions || {}) } : {}), mergeObjects({}, config && config.reactionOptions ? config.reactionOptions : {}) ).draw(smiles, svg, themeName, function() { done(); }, function(error) { fail(error); }, config && config.reactionWeights ? config.reactionWeights : null); } else if (config && config.weights && config.weights.length) { new SmiDrawer( mergeObjects({}, profile.options, { weights: mergeObjects({ sigma: autoSigma, opacity: 0.72, additionalPadding: 30 }, config.weightOptions || {}) }), mergeObjects({}, config && config.reactionOptions ? config.reactionOptions : {}) ).draw(smiles, svg, themeName, null, null, config.weights); done(); } else if (config && config.highlights && config.highlights.length) { SmilesDrawer.parse(smiles, function(tree) { new SmilesDrawer.SvgDrawer(profile.options).draw( tree, svg, themeName, null, false, config.highlights ); done({tree: tree}); }, function(error) { fail(error); }); } else { SmilesDrawer.parse(smiles, function(tree) { new SmilesDrawer.SvgDrawer(profile.options).draw(tree, svg, themeName); done({tree: tree}); }, function(error) { fail(error); }); } } catch (error) { fail(error); } return { kind: kind, metrics: profile.metrics, options: profile.options, svg: svg }; } window.SmilesWebsite = { applySvgBox: applySvgBox, applyThemeSurface: applyThemeSurface, buildMoleculeOptions: buildMoleculeOptions, buildReactionOptions: buildReactionOptions, getDefaultRenderOptions: getDefaultRenderOptions, getPageTheme: getPageTheme, getRenderConfig: getRenderConfig, isReaction: isReaction, mergeObjects: mergeObjects, renderSmiles: renderSmiles, resetSvg: resetSvg, waitForFonts: waitForFonts }; })(window);