I am creating 2D molecule visualizations using a “molfile”, which is a collection of x,y,z points for atoms (I just use the x and y points for 2D renderings), and a collection of bonds linking the atoms. I create circles for all non-carbon atoms, and lines between atoms for bonds, in SVG. However, it looks like this currently:
If I used 1px lines, it would probably work, but I want to use thick lines. What approach(es) can I take to mitigate this problem?
- I don’t know from the raw data directly what direction to the next bond goes in, so it’s not as if I can simply create one large SVG
path
(unless I am missing something). Also, the path would have to form loops like the hexagon above. - I want to color certain bonds different colors, so it’s going to have to let me change individual bond colors in the end (whatever structure may be the final result.
Here are two examples from the wild which demonstrate different styles I might want to do (but while also using thicker bond lines than they do):
(I like the second one better).
So how can I make the SVG look more like the second one, but with thicker lines, so there are not wedges “missing” between the lines like my first image above?
My React.js code to render this is essentially this:
// other inspiration here:
// https://github.com/BoboRett/MolViewer/blob/master/molViewer.js
ReactDOM.render(
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 200 200"
>
<Molecule2D
molfile={`
Marvin 03060614192D
11 11 0 0 0 0 999 V2000
-1.0392 -0.6000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
-0.3248 -1.0125 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
0.3897 -0.6000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
0.3897 0.2250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
-0.3248 0.6375 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
-1.0392 0.2250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
-1.7537 -1.0125 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0
-1.7537 0.6375 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0
1.1042 0.6375 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
1.8187 0.2250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
2.5331 0.6375 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0
2 1 1 0 0 0 0
6 1 2 0 0 0 0
7 1 1 0 0 0 0
3 2 2 0 0 0 0
4 3 1 0 0 0 0
5 4 2 0 0 0 0
9 4 1 0 0 0 0
6 5 1 0 0 0 0
8 6 1 0 0 0 0
10 9 1 0 0 0 0
11 10 1 0 0 0 0
M END
`}
/>
</svg>,
document.body
);
type Molecule2DInput = {
bondWidth?: number;
atomRadius?: number;
scale?: number;
molfile: string;
};
const Molecule2D: React.FC<Molecule2DInput> = ({
bondWidth = 12,
atomRadius = 16,
scale = 32,
molfile,
}) => {
const { atoms, bonds } = parseMolfile({ text: molfile });
const elements: Array<React.ReactNode> = [];
// Find minimum x and y coordinates
const xs = atoms.map((atom) => atom.x);
const ys = atoms.map((atom) => atom.y);
const minX = Math.min(...xs);
const minY = Math.min(...ys);
const maxX = Math.max(...xs);
const maxY = Math.max(...ys);
const offset = bondWidth + atomRadius;
// Translate all coordinates
const translatedAtoms = atoms.map((atom) => ({
...atom,
x: atom.x - minX,
y: atom.y - minY,
}));
let key = 1;
// Draw bonds
for (const bond of bonds) {
const startAtom = translatedAtoms[bond.start];
const endAtom = translatedAtoms[bond.end];
elements.push(
<line
key={`k-${key++}`}
x1={startAtom.x * scale + offset}
y1={startAtom.y * scale + offset}
x2={endAtom.x * scale + offset}
y2={endAtom.y * scale + offset}
stroke="#555555"
strokeWidth={bondWidth}
/>
);
}
// Draw atoms
for (const atom of translatedAtoms) {
if (atom.symbol === "C" || atom.symbol === "H") {
continue;
}
elements.push(
<circle
key={`k-${key++}`}
cx={atom.x * scale + offset}
cy={atom.y * scale + offset}
r={atomRadius}
fill="#555555"
/>
);
// elements.push(
// `<text x="${atom.x}" y="${atom.y}" font-size="10" text-anchor="middle" dy=".3em">${atom.symbol}</text>`,
// )
}
return (
<g>
<rect
width={maxX * scale - minX * scale + offset}
height={maxY * scale - minY * scale + offset}
fill="transparent"
style={{ pointerEvents: "none" }}
/>
{elements}
</g>
);
};
type Atom = {
symbol: string;
x: number;
y: number;
};
type Bond = {
start: number;
end: number;
number: number;
};
function parseMolfile({ text }: { text: string }) {
const lines = text.split("n");
const atoms: Array<Atom> = [];
const bonds: Array<Bond> = [];
// Skipping the header lines
const countsLine = lines[3];
const atomCount = parseInt(countsLine.substring(0, 3).trim());
const bondCount = parseInt(countsLine.substring(3, 6).trim());
// Reading atoms
for (let i = 4; i < 4 + atomCount; i++) {
const line = lines[i];
const x = parseFloat(line.substring(0, 10).trim());
const y = parseFloat(line.substring(10, 20).trim());
const symbol = line.substring(31, 34).trim();
atoms.push({ symbol, x, y });
}
// Reading bonds
for (let i = 4 + atomCount; i < 4 + atomCount + bondCount; i++) {
const line = lines[i];
const start = parseInt(line.substring(0, 3).trim()) - 1;
const end = parseInt(line.substring(3, 6).trim()) - 1;
const number = parseInt(line.substring(6, 9).trim()) - 1;
bonds.push({ start, end, number });
}
return { atoms, bonds };
}
That points to at least how I am rendering the SVG bonds, as line
elements:
<line
x1={startAtom.x * scale + offset}
y1={startAtom.y * scale + offset}
x2={endAtom.x * scale + offset}
y2={endAtom.y * scale + offset}
stroke="#555555"
strokeWidth={bondWidth}
/>
Any suggestions?