Skip to content

Commit

Permalink
feat(clickable): add bond click (#74)
Browse files Browse the repository at this point in the history
* feat(clickable): add bond click

See  #73

* chore: v1.5.0-beta.1

* fix(clickable): ignore clickable bonds and highlight bonds when computing hitboxes

See #73

* chore: v1.5.0
  • Loading branch information
RamziWeslati authored Apr 13, 2023
1 parent f7b6aa2 commit 30c40f0
Show file tree
Hide file tree
Showing 9 changed files with 334 additions and 73 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@ const props: MoleculeRepresentationProps = {
],
height: 200,
width: 300,
onAtomClick: (atomId: string) => console.log(atomId),
onAtomClick: (atomId: string) => console.log("clicked atoms idx:", atomId),
onBondClick: (bondIdentifier: ClickedBondIdentifiers) => {
console.log("clicked bond idx:", bondIdentifier.bondId)
console.log("clicked bond starting atom idx:", bondIdentifier.startAtomId)
console.log("clicked bond ending atom idx:", bondIdentifier.endAtomId)
}
zoomable: true
};
<MoleculeRepresentation {...props} onAtomClick={} />
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@iktos-oss/molecule-representation",
"version": "1.4.3",
"version": "1.5.0",
"description": "exports interactif molecule represnetations as react components",
"main": "lib/cjs/index.js",
"module": "lib/esm/index.js",
Expand Down
67 changes: 44 additions & 23 deletions src/components/MoleculeRepresentation/MoleculeRepresentation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,25 @@

import React, { CSSProperties, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { RDKitColor, 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 {
CLICKABLE_MOLECULE_CLASSNAME,
computeClickingAreaForAtoms,
getAtomIdxFromClickableId,
} from './MoleculeRepresentation.service';
import { ZoomWrapper, DisplayZoomToolbar, DisplayZoomToolbarStrings } from '../Zoom';
import { Spinner } from '../Spinner';
import { isEqual } from '../../utils/compare';
import { createSvgElement } from '../../utils/create-svg-element';
import {
ClickableAtoms,
DrawSmilesSVGProps,
get_svg,
get_svg_from_smarts,
appendHitboxesToSvg,
buildAtomsHitboxes,
buildBondsHitboxes,
isIdClickedABond,
getClickedBondIdentifiersFromId,
isIdClickedAnAtom,
getAtomIdxFromClickableId,
CLICKABLE_MOLECULE_CLASSNAME,
ClickedBondIdentifiers,
} from '../../utils';

export type MoleculeRepresentationProps = SmilesRepresentationProps | SmartsRepresentationProps;

Expand All @@ -49,6 +56,7 @@ export const MoleculeRepresentation: React.FC<MoleculeRepresentationProps> = mem
height,
id,
onAtomClick,
onBondClick,
smarts,
smiles,
alignmentDetails,
Expand All @@ -63,8 +71,9 @@ export const MoleculeRepresentation: React.FC<MoleculeRepresentationProps> = mem
const { worker } = useRDKit();
const moleculeRef = useRef<SVGElement>(null);
const [svgContent, setSvgContent] = useState('');
const [rects, setRects] = useState<Array<Rect>>([]);
const isClickable = useMemo(() => !!onAtomClick, [onAtomClick]);
const [atomsHitbox, setAtomsHitbox] = useState<Array<SVGRectElement>>([]);
const [bondsHitbox, setBondsHitbox] = useState<SVGPathElement[]>([]);
const isClickable = useMemo(() => !!onAtomClick || !!onBondClick, [onAtomClick, onBondClick]);
const [shouldComputeRectsDetails, setShouldComputeRectsDetails] = useState<{
shouldComputeRects: boolean;
computedRectsForAtoms: number[];
Expand All @@ -79,19 +88,24 @@ export const MoleculeRepresentation: React.FC<MoleculeRepresentationProps> = mem
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);
if (onAtomClick) {
buildAtomsHitboxes({
numAtoms: moleculeDetails.numAtoms,
parentDiv: moleculeRef.current,
clickableAtoms: clickableAtoms?.clickableAtomsIds,
}).then(setAtomsHitbox);
}
if (onBondClick) {
buildBondsHitboxes(moleculeDetails.numAtoms, moleculeRef.current).then(setBondsHitbox);
}
},
100,
);
setShouldComputeRectsDetails({
shouldComputeRects: false,
computedRectsForAtoms: clickableAtoms?.clickableAtomsIds ?? [...Array(moleculeDetails.numAtoms).keys()],
});
}, [smiles, smarts, isClickable, clickableAtoms, worker]);
}, [worker, isClickable, smiles, smarts, clickableAtoms?.clickableAtomsIds, onAtomClick, onBondClick]);

useEffect(() => {
if (!shouldComputeRectsDetails.shouldComputeRects) return;
Expand Down Expand Up @@ -119,7 +133,8 @@ export const MoleculeRepresentation: React.FC<MoleculeRepresentationProps> = mem
? 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;
const svgWithHitBoxes =
atomsHitbox.length || bondsHitbox.length ? appendHitboxesToSvg(svg, atomsHitbox, bondsHitbox) : svg;
if (svgWithHitBoxes) {
setSvgContent(svgWithHitBoxes);
}
Expand All @@ -138,7 +153,8 @@ export const MoleculeRepresentation: React.FC<MoleculeRepresentationProps> = mem
showSmartsAsSmiles,
smiles,
smarts,
rects,
atomsHitbox,
bondsHitbox,
atomsToHighlight,
addAtomIndices,
details,
Expand All @@ -154,14 +170,18 @@ export const MoleculeRepresentation: React.FC<MoleculeRepresentationProps> = mem
const handleOnClick = useCallback(
(e: React.MouseEvent) => {
const clickedId = (e.target as SVGRectElement).id;
if (onAtomClick && clickedId) {
if (isClickable) {
e.preventDefault();
e.stopPropagation();
const atomIdx = getAtomIdxFromClickableId(clickedId);
onAtomClick(atomIdx);
}
if (onBondClick && clickedId && isIdClickedABond(clickedId)) {
onBondClick(getClickedBondIdentifiersFromId(clickedId));
}
if (onAtomClick && clickedId && isIdClickedAnAtom(clickedId)) {
onAtomClick(getAtomIdxFromClickableId(clickedId));
}
},
[onAtomClick],
[onAtomClick, onBondClick, isClickable],
);

if (!svgContent) {
Expand All @@ -173,7 +193,7 @@ export const MoleculeRepresentation: React.FC<MoleculeRepresentationProps> = mem
'data-testid': 'clickable-molecule',
ref: moleculeRef,
...restOfProps,
className: `molecule ${onAtomClick ? CLICKABLE_MOLECULE_CLASSNAME : ''}`,
className: `molecule ${isClickable ? CLICKABLE_MOLECULE_CLASSNAME : ''}`,
height,
id,
onClick: handleOnClick,
Expand Down Expand Up @@ -205,6 +225,7 @@ interface MoleculeRepresentationBaseProps {
height: number;
id?: string;
onAtomClick?: (atomId: string) => void;
onBondClick?: (clickedBondIdentifiers: ClickedBondIdentifiers) => void;
style?: CSSProperties;
showLoadingSpinner?: boolean;
showSmartsAsSmiles?: boolean;
Expand Down
106 changes: 103 additions & 3 deletions src/stories/MoleculeRepresentation.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { RDKitColor, RDKitProvider } from '@iktos-oss/rdkit-provider';
import { BIG_MOLECULE, MOLECULES, RANOLAZINE_SMILES, SEVEN_HIGHLIGHTS_RANOLAZINE } from './fixtures/molecules';
import { CCO_MOL_BLOCK, SMILES_TO_ALIGN_CCO_AGAINST } from './fixtures/molblock';
import { RDKitProviderProps } from '@iktos-oss/rdkit-provider';
import { ClickedBondIdentifiers } from '../utils';

export default {
title: 'components/molecules/MoleculeRepresentation',
Expand All @@ -45,6 +46,7 @@ const PROPS: MoleculeRepresentationProps = {
height: 200,
width: 300,
onAtomClick: undefined,
onBondClick: undefined,
zoomable: false,
};

Expand Down Expand Up @@ -115,6 +117,94 @@ const TemplateWithOnAtomClick: Story<MoleculeRepresentationProps> = (args) => {
</RDKitProvider>
);
};
const TemplateWithOnBondClick: Story<MoleculeRepresentationProps> = (args) => {
const [_, updateArgs] = useArgs();

const onBondClick = (identifiers: ClickedBondIdentifiers) => {
const clickedBondId = parseInt(identifiers.bondId);
if (args.bondsToHighlight?.flat().includes(clickedBondId)) {
updateArgs({
...args,
bondsToHighlight: args.bondsToHighlight.map((highlightedBondssColor) =>
highlightedBondssColor.filter((id) => id !== clickedBondId),
),
});
} else {
updateArgs({
...args,
bondsToHighlight: !args.bondsToHighlight
? [[clickedBondId]]
: args.bondsToHighlight.map((highlightedBondssColor, colorIdx) => {
if (colorIdx === 0) {
return [...highlightedBondssColor, clickedBondId];
}
return highlightedBondssColor;
}),
});
}
};
return (
<RDKitProvider {...RDKitProviderCachingProps}>
<MoleculeRepresentation {...args} onBondClick={onBondClick} />
</RDKitProvider>
);
};

const TemplateWithOnAtomAndBondClick: Story<MoleculeRepresentationProps> = (args) => {
const [_, updateArgs] = useArgs();

const onAtomClick = (atomId: string) => {
const clickedAtomId = parseInt(atomId);
if (args.atomsToHighlight?.flat().includes(clickedAtomId)) {
updateArgs({
...args,
atomsToHighlight: args.atomsToHighlight.map((highlightedAtomsColor) =>
highlightedAtomsColor.filter((id) => id !== clickedAtomId),
),
});
} else {
updateArgs({
...args,
atomsToHighlight: !args.atomsToHighlight
? [[clickedAtomId]]
: args.atomsToHighlight.map((highlightedAtomsColor, colorIdx) => {
if (colorIdx === 0) {
return [...highlightedAtomsColor, clickedAtomId];
}
return highlightedAtomsColor;
}),
});
}
};
const onBondClick = (identifiers: ClickedBondIdentifiers) => {
const clickedBondId = parseInt(identifiers.bondId);
if (args.bondsToHighlight?.flat().includes(clickedBondId)) {
updateArgs({
...args,
bondsToHighlight: args.bondsToHighlight.map((highlightedBondssColor) =>
highlightedBondssColor.filter((id) => id !== clickedBondId),
),
});
} else {
updateArgs({
...args,
bondsToHighlight: !args.bondsToHighlight
? [[clickedBondId]]
: args.bondsToHighlight.map((highlightedBondssColor, colorIdx) => {
if (colorIdx === 0) {
return [...highlightedBondssColor, clickedBondId];
}
return highlightedBondssColor;
}),
});
}
};
return (
<RDKitProvider {...RDKitProviderCachingProps}>
<MoleculeRepresentation {...args} onAtomClick={onAtomClick} onBondClick={onBondClick} />
</RDKitProvider>
);
};

export const Default = Template.bind({});
Default.args = { moleculeRepresetnationProps: PROPS, rdkitProviderProps: RDKitProviderCachingProps };
Expand Down Expand Up @@ -218,12 +308,22 @@ WithSubstructureAlignmentTemplate.args = {
listOfSmiles: SMILES_TO_ALIGN_CCO_AGAINST,
};

export const Clickable = TemplateWithOnAtomClick.bind({});
Clickable.args = {
export const ClickableAtoms = TemplateWithOnAtomClick.bind({});
ClickableAtoms.args = {
...PROPS,
};

export const ClickableBonds = TemplateWithOnBondClick.bind({});
ClickableBonds.args = {
...PROPS,
};

export const ClickableBondsAndAtoms = TemplateWithOnAtomAndBondClick.bind({});
ClickableBondsAndAtoms.args = {
...PROPS,
};

export const BigClickableMoleculeWithLoadingSpinner = TemplateWithOnAtomClick.bind({});
export const BigClickableMoleculeWithLoadingSpinner = TemplateWithOnAtomAndBondClick.bind({});
BigClickableMoleculeWithLoadingSpinner.args = {
...PROPS,
width: 800,
Expand Down
Loading

0 comments on commit 30c40f0

Please sign in to comment.