Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use a web worker for all rdkit operations #50

Merged
merged 9 commits into from
Mar 17, 2023
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
lib
node_modules
tsconfig.tsbuildinfo
public/RDKit_minimal.*
public/rdkit-worker.js
storybook-static
21 changes: 10 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@iktos-oss/molecule-representation",
"version": "1.2.3",
"version": "1.3.0",
"description": "exports interactif molecule represnetations as react components",
"main": "lib/cjs/index.js",
"module": "lib/esm/index.js",
Expand Down Expand Up @@ -36,13 +36,14 @@
"tsc-check": "tsc --noEmit --skipLibCheck",
"dist-package": "./tools/dist-package.sh",
"test-dist-package": "./tools/test-dist-package.sh",
"storybook": "start-storybook -p 6006",
"storybook": "npm run prerun:cp-assets && start-storybook -s ./public -p 6006",
"prerun:cp-assets": "cp node_modules/@rdkit/rdkit/dist/RDKit_minimal.js node_modules/@rdkit/rdkit/dist/RDKit_minimal.wasm node_modules/@iktos-oss/rdkit-provider/lib/rdkit-worker.js ./public",
"build-storybook": "build-storybook"
},
"author": "Ramzi Oueslati <ramzi.oueslati@iktos.com>",
"devDependencies": {
"@babel/core": "^7.20.12",
"@iktos-oss/rdkit-provider": "^1.2.3",
"@iktos-oss/rdkit-provider": "^2.0.0",
"@rdkit/rdkit": "^2022.9.3-1.0.0",
"@storybook/addon-actions": "^6.5.16",
"@storybook/addon-essentials": "^6.5.16",
Expand Down Expand Up @@ -75,9 +76,9 @@
"typescript": "^4.9.4"
},
"peerDependencies": {
"@iktos-oss/rdkit-provider": "^2.0.0",
"@rdkit/rdkit": "^2022.9.3-1.0.0",
"react": ">=17.0.2",
"react-dom": ">=17.0.2",
"@iktos-oss/rdkit-provider": "^1.2.3"
"react-dom": ">=17.0.2"
}
}
80 changes: 49 additions & 31 deletions src/components/MoleculeRepresentation/MoleculeRepresentation.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React, { CSSProperties, memo, useEffect, useMemo, useRef, useState } from 'react';
import { useRDKit } from '@iktos-oss/rdkit-provider';
import React, { CSSProperties, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { getMoleculeDetails, isValidSmiles, useRDKit } from '@iktos-oss/rdkit-provider';
import { ClickableAtoms, DrawSmilesSVGProps, get_svg, get_svg_from_smarts } from '../../utils/draw';
import { appendRectsToSvg, Rect } from '../../utils/html';
import { get_molecule_details, is_valid_smiles } from '../../utils/molecule';

import {
CLICKABLE_MOLECULE_CLASSNAME,
Expand Down Expand Up @@ -30,56 +29,74 @@ export const MoleculeRepresentation: React.FC<MoleculeRepresentationProps> = mem
alignmentDetails,
style,
showLoadingSpinner = false,
showSmartsAsSmiles = false,
width,
...restOfProps
}: MoleculeRepresentationProps) => {
const { RDKit } = useRDKit();
const { worker } = useRDKit();
const moleculeRef = useRef<HTMLDivElement>(null);
const [svgContent, setSvgContent] = useState('');
const [rects, setRects] = useState<Array<Rect>>([]);
const isClickable = useMemo(() => !!onAtomClick, [onAtomClick]);
const [shouldComputeRects, setShouldComputeRects] = useState(false);

useEffect(() => {
if (!RDKit) return;
const computeClickingRects = useCallback(async () => {
if (!worker) return;
if (!isClickable) return;
const structureToDraw = smiles || (smarts as string);
const moleculeDetails = get_molecule_details(structureToDraw, RDKit);
const moleculeDetails = await getMoleculeDetails(worker, { smiles: structureToDraw });
if (!moleculeDetails) return;
setTimeout(
// do this a better way, the issue is when highlighting there is a moment when the atom-0 is rendered at the wrong position (0-0)
() =>
() => {
computeClickingAreaForAtoms({
numAtoms: moleculeDetails.numAtoms,
parentDiv: moleculeRef.current,
clickableAtoms: clickableAtoms?.clickableAtomsIds,
}).then(setRects),
}).then(setRects);
},
100,
);
}, [smiles, smarts, RDKit, isClickable, clickableAtoms]);
setShouldComputeRects(false);
}, [smiles, smarts, isClickable, clickableAtoms, worker]);

useEffect(() => {
if (!shouldComputeRects) return;
computeClickingRects();
}, [shouldComputeRects, computeClickingRects]);

useEffect(() => {
if (!RDKit) return;
const drawingDetails: DrawSmilesSVGProps = {
smiles: smarts || (smiles as string),
width,
height,
details: { ...details, addAtomIndices },
alignmentDetails,
atomsToHighlight,
bondsToHighlight,
isClickable,
clickableAtoms,
if (!worker) return;
const computeSvg = async () => {
const drawingDetails: DrawSmilesSVGProps = {
smiles: (smarts || smiles) as string,
width,
height,
details: { ...details, addAtomIndices },
alignmentDetails,
atomsToHighlight,
bondsToHighlight,
isClickable,
clickableAtoms,
};
const isSmartsAValidSmiles =
showSmartsAsSmiles && !!smarts && (await isValidSmiles(worker, { smiles: smarts })).isValid;
const svg =
smarts && !isSmartsAValidSmiles
? await get_svg_from_smarts({ smarts, width, height }, worker)
: await get_svg(drawingDetails, worker);
if (!svg) return;
const svgWithHitBoxes = rects.length ? appendRectsToSvg(svg, rects) : svg;
if (svgWithHitBoxes) {
setSvgContent(svgWithHitBoxes);
}
if (!rects.length && isClickable) {
setShouldComputeRects(true);
}
};
setTimeout(() => {
// put workload in a settimeout 0 to avoid blocking the main thread when rendering lots of molecules
const svg = smarts
? is_valid_smiles(smarts, RDKit)
? get_svg(drawingDetails, RDKit)
: get_svg_from_smarts({ smarts, width, height }, RDKit)
: get_svg(drawingDetails, RDKit);
if (svg) setSvgContent(appendRectsToSvg(svg, rects));
}, 0);
computeSvg();
}, [
showSmartsAsSmiles,
smiles,
smarts,
rects,
Expand All @@ -90,7 +107,7 @@ export const MoleculeRepresentation: React.FC<MoleculeRepresentationProps> = mem
bondsToHighlight,
width,
height,
RDKit,
worker,
clickableAtoms,
alignmentDetails,
]);
Expand Down Expand Up @@ -135,6 +152,7 @@ interface MoleculeRepresentationBaseProps {
onAtomClick?: (atomId: string) => void;
style?: CSSProperties;
showLoadingSpinner?: boolean;
showSmartsAsSmiles?: boolean;
width: number;
}

Expand Down
19 changes: 17 additions & 2 deletions src/stories/MoleculeRepresentation.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,21 @@ export const FromSmarts = Template.bind({});
FromSmarts.args = {
moleculeRepresetnationProps: {
...PROPS,
smarts: undefined,
smiles: '****CO',
smarts: '[#6][CH1](=O)',
smiles: undefined,
alignmentDetails: undefined,
},
rdkitProviderProps: RDKitProviderCachingProps,
};

export const DrawSmartsAsSmiles = Template.bind({});
DrawSmartsAsSmiles.args = {
moleculeRepresetnationProps: {
...PROPS,
smarts: '[#6][CH1](=O)',
showSmartsAsSmiles: true,
smiles: undefined,
alignmentDetails: undefined,
},
rdkitProviderProps: RDKitProviderCachingProps,
};
Expand Down Expand Up @@ -180,6 +193,8 @@ Clickable.args = {
export const BigClickableMoleculeWithLoadingSpinner = TemplateWithOnAtomClick.bind({});
BigClickableMoleculeWithLoadingSpinner.args = {
...PROPS,
width: 800,
height: 800,
smiles: BIG_MOLECULE,
showLoadingSpinner: true,
};
Expand Down
Loading