Depicting CSV or SDF in HTML
Problem
You want to depict molecules along with their associated data read from a
CSV file in an HTML file.
See the generated HTML file in drugs.html
and its screenshot in Figure 1.
Figure 1. Example of depicting CSV in HTML (The screenshot is reduced here for visualization convenience)
Ingredients
|
Difficulty Level
🌶️ 🌶️
Download
Source Code
csv2html
#!/usr/bin/env python3
# (C) 2026 Cadence Design Systems, Inc. (Cadence)
# All rights reserved.
# TERMS FOR USE OF SAMPLE CODE The software below ("Sample Code") is
# provided to current licensees or subscribers of Cadence products or
# SaaS offerings (each a "Customer").
# Customer is hereby permitted to use, copy, and modify the Sample Code,
# subject to these terms. Cadence claims no rights to Customer's
# modifications. Modification of Sample Code is at Customer's sole and
# exclusive risk. Sample Code may require Customer to have a then
# current license or subscription to the applicable Cadence offering.
# THE SAMPLE CODE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED. CADENCE DISCLAIMS ALL WARRANTIES, INCLUDING, BUT
# NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
# PARTICULAR PURPOSE AND NONINFRINGEMENT. In no event shall Cadence be
# liable for any damages or liability in connection with the Sample Code
# or its use.
"""Converts a CSV or SDF file into an HTML file with molecular depictions."""
import argparse
import os
import pathlib
import sys
import typing
from openeye import oechem, oedepict
from rich_argparse import HelpPreviewAction, RichHelpFormatter
__SCRIPT_NAME__ = pathlib.Path(__file__).absolute().stem
__SCRIPT_DESC__ = "Convert CSV or SDF files into HTML with molecular depictions"
__SCRIPT_TOOLKITS__ = ["oechem", "oedepict"]
__SCRIPT_CATEGORIES__ = ["depiction"]
def parse_options() -> argparse.Namespace:
"""Set up command line options."""
parser = argparse.ArgumentParser(
add_help=True,
formatter_class=RichHelpFormatter,
description="[yellow]" + __SCRIPT_DESC__ + "[/yellow]",
)
io_group = parser.add_argument_group("Input/output options")
io_group.add_argument(
"--mol",
metavar="MOL-FILE",
type=str,
required=True,
help="input molecule file (.csv or .sdf)",
)
io_group.add_argument(
"--html",
metavar="HTML-FILE",
type=str,
required=True,
help="output HTML file (.html)",
)
parser.add_argument("--help-image", action=HelpPreviewAction)
parser.add_argument(
"--save-console-svg",
default=False,
action="store_true",
help=f"run command and capture console output in {__SCRIPT_NAME__}.svg file",
)
return parser.parse_args()
def main() -> int:
"""Convert molecule file to HTML with molecular depictions."""
args = parse_options()
html_file = pathlib.Path(args.html)
if html_file.suffix.lower() != ".html":
oechem.OEThrow.Fatal("Invalid file extension expected .html!")
mol_list: list[oechem.OEMolBase] = read_molecules(args.mol)
tags = collect_data_tags(mol_list)
width, height = 200, 200
opts = oedepict.OE2DMolDisplayOptions(width, height, oedepict.OEScale_AutoScale)
with html_file.open("w") as ofp:
_write_html_file(ofp, mol_list, pathlib.Path(args.mol).name, tags, opts)
return os.EX_OK
def read_molecules(mol_filename: str) -> list[oechem.OEMolBase]:
"""Read molecules from a CSV or SDF file and return a list of OEMolBase objects."""
mol_path = pathlib.Path(mol_filename)
if not mol_path.exists():
oechem.OEThrow.Fatal(f"Cannot open input file '{mol_path.name}'!")
ifs = oechem.oemolistream()
if not ifs.open(str(mol_path)):
oechem.OEThrow.Fatal(f"Cannot open input file '{mol_path.name}'!")
if ifs.GetFormat() not in [oechem.OEFormat_CSV, oechem.OEFormat_SDF]:
oechem.OEThrow.Fatal("Input must be a CSV or SDF file!")
mol_list: list[oechem.OEMolBase] = [
oechem.OEGraphMol(m) for m in ifs.GetOEGraphMols()
]
return mol_list
def collect_data_tags(mol_list: list[oechem.OEMolBase]) -> list[str]:
"""Collect all unique SD data tags from a list of molecules."""
tags: list[str] = []
for mol in mol_list:
for dp in oechem.OEGetSDDataIter(mol):
if dp.GetTag() not in tags:
tags.append(dp.GetTag())
return tags
def _write_html_file(
ofp: typing.TextIO,
mol_list: list[oechem.OEMolBase],
filename: str,
tags: list[str],
opts: oedepict.OE2DMolDisplayOptions,
) -> None:
"""Write the full HTML file content."""
_write_html_header(ofp, filename, tags)
for mol in mol_list:
_write_html_table_row(ofp, mol, opts, tags)
_write_html_footer(ofp)
def _write_html_table_row(
ofp: typing.TextIO,
mol: oechem.OEMolBase,
opts: oedepict.OE2DMolDisplayOptions,
tags: list[str],
) -> None:
"""Write one table row with the molecule image and its SD data."""
ofp.write("<tr class=row>\n")
ofp.write(f"<td> {_get_svg_image(mol, opts)}\n </td>\n")
for tag in tags:
value = oechem.OEGetSDData(mol, tag) if oechem.OEHasSDData(mol, tag) else "N/A"
ofp.write(f"<td> {value} </td>")
ofp.write("</tr>\n")
def _get_svg_image(mol: oechem.OEMolBase, opts: oedepict.OE2DMolDisplayOptions) -> str:
"""Generate a bare SVG image string for a molecule."""
oedepict.OEPrepareDepiction(mol)
disp = oedepict.OE2DMolDisplay(mol, opts)
image_str = oedepict.OERenderMoleculeToString("bsvg", disp, False)
return image_str.decode("utf-8")
def _write_html_header(ofp: typing.TextIO, filename: str, tags: list[str]) -> None:
"""Write the HTML header including CSS styles and table header row."""
table_width = min(1800, (len(tags) + 1) * 200)
ofp.write("<style type='text/css'>\n")
ofp.write(
"h1 { text-align:center; border-width:thick; border-style:double;border-color:#BCC; }\n"
)
ofp.write(
f"table.csv {{ border-spacing:1; background: #FFF; width:{table_width}px; }}\n"
)
ofp.write(
"table.csv td, th { width:100px; text-align:center; padding:3px 3px 3px 3px; }\n"
)
ofp.write(
"table.csv th { height:50px; color:#FFF; background:#788; }\n"
)
ofp.write("table.csv tr:nth-child(even){ color:#000; background:#FFE; }\n")
ofp.write("table.csv tr:nth-child(odd) { color:#000; background:#FEF; }\n")
ofp.write("table.csv tr:hover { color:#000; background:#DDD; }\n")
ofp.write("</style>\n")
ofp.write(f"<html><h1>{filename}</h1>\n")
ofp.write("<body>\n")
ofp.write("<table class=csv>\n")
ofp.write("<tbody>\n")
ofp.write("<tr>\n")
ofp.write("<th> Molecule</th>")
ofp.writelines(f"<th> {tag} </th>" for tag in tags)
ofp.write("\n</tr>\n")
def _write_html_footer(ofp: typing.TextIO) -> None:
"""Write the HTML closing tags."""
ofp.write("</table>\n</body>\n</html>\n")
setattr(main, "__SCRIPT_NAME__", __SCRIPT_NAME__)
setattr(main, "__SCRIPT_DESC__", __SCRIPT_DESC__)
setattr(main, "__SCRIPT_TOOLKITS__", __SCRIPT_TOOLKITS__)
setattr(main, "__SCRIPT_CATEGORIES__", __SCRIPT_CATEGORIES__)
if __name__ == "__main__":
sys.exit(main())
Solution
The CSV file format is a text file format containing comma-separated values. In OEChem TK, this file format is implemented to enable data exchange with a wide variety of other software. Each line of a CSV file stores data for a molecule that is represented by a SMILES string.
See also
CSV File Format section of the OEChem TK documentation about the layout of the CSV file format.
When reading a CSV file, the fields of the file are attached to each molecule as SD data. This data can be accessed by the OEGetSDDataIter function that returns an iterator over all the SD data (tag - value) pairs of a molecule. The collect_data_tags function iterates over a list of molecules and returns the unique tags of the data attached to the molecules.
def collect_data_tags(mol_list: list[oechem.OEMolBase]) -> list[str]:
"""Collect all unique SD data tags from a list of molecules."""
tags: list[str] = []
for mol in mol_list:
for dp in oechem.OEGetSDDataIter(mol):
if dp.GetTag() not in tags:
tags.append(dp.GetTag())
return tags
The _write_html_file
function takes a list of molecules read from a CSV file along with the
data tags returned by the collect_data_tags
function.
It first writes the header of the html file,
followed by iterating over the molecules and adding a new row into a table for
each molecule by calling the _write_html_table_row
function.
Finally, it finishes writing the html file by calling the
_write_html_footer function.
def _write_html_file(
ofp: typing.TextIO,
mol_list: list[oechem.OEMolBase],
filename: str,
tags: list[str],
opts: oedepict.OE2DMolDisplayOptions,
) -> None:
"""Write the full HTML file content."""
_write_html_header(ofp, filename, tags)
for mol in mol_list:
_write_html_table_row(ofp, mol, opts, tags)
_write_html_footer(ofp)
The _write_html_header function sets
the style of the html file and then adds the header of
a table in which the molecules along with their data will be inserted.
def _write_html_header(ofp: typing.TextIO, filename: str, tags: list[str]) -> None:
"""Write the HTML header including CSS styles and table header row."""
table_width = min(1800, (len(tags) + 1) * 200)
ofp.write("<style type='text/css'>\n")
ofp.write(
"h1 { text-align:center; border-width:thick; border-style:double;border-color:#BCC; }\n"
)
ofp.write(
f"table.csv {{ border-spacing:1; background: #FFF; width:{table_width}px; }}\n"
)
ofp.write(
"table.csv td, th { width:100px; text-align:center; padding:3px 3px 3px 3px; }\n"
)
ofp.write(
"table.csv th { height:50px; color:#FFF; background:#788; }\n"
)
ofp.write("table.csv tr:nth-child(even){ color:#000; background:#FFE; }\n")
ofp.write("table.csv tr:nth-child(odd) { color:#000; background:#FEF; }\n")
ofp.write("table.csv tr:hover { color:#000; background:#DDD; }\n")
ofp.write("</style>\n")
ofp.write(f"<html><h1>{filename}</h1>\n")
ofp.write("<body>\n")
ofp.write("<table class=csv>\n")
ofp.write("<tbody>\n")
ofp.write("<tr>\n")
ofp.write("<th> Molecule</th>")
ofp.writelines(f"<th> {tag} </th>" for tag in tags)
ofp.write("\n</tr>\n")
The _write_html_table_row function inserts
the image of the molecule along with its corresponding data
into the next row of the html table.
def _write_html_table_row(
ofp: typing.TextIO,
mol: oechem.OEMolBase,
opts: oedepict.OE2DMolDisplayOptions,
tags: list[str],
) -> None:
"""Write one table row with the molecule image and its SD data."""
ofp.write("<tr class=row>\n")
ofp.write(f"<td> {_get_svg_image(mol, opts)}\n </td>\n")
for tag in tags:
value = oechem.OEGetSDData(mol, tag) if oechem.OEHasSDData(mol, tag) else "N/A"
ofp.write(f"<td> {value} </td>")
ofp.write("</tr>\n")
The _get_svg_image function generates a molecule
display and returns its image as a string in bare svg image file format (with no header).
This image string can be directly inserted into an html file.
def _get_svg_image(mol: oechem.OEMolBase, opts: oedepict.OE2DMolDisplayOptions) -> str:
"""Generate a bare SVG image string for a molecule."""
oedepict.OEPrepareDepiction(mol)
disp = oedepict.OE2DMolDisplay(mol, opts)
image_str = oedepict.OERenderMoleculeToString("bsvg", disp, False)
return image_str.decode("utf-8")
The _write_html_footer function
simply closes the table and the body of the html file.
Usage
See Download section to download the script.
> csv2html --help
Running the above command with drugs.csv will generate the
drugs.html file.
> csv2html --mol drugs.csv --html drugs.html
Discussion
Reading the columns of a CSV file into SD data fields means
that the OEChem TK provides a meta-data interchange between sdf files and
CSV files.
Consequently, the same Python script can be used to generate an html file
reading an sdf file.
Running the above command with drugs.sdf will generate the same
drugs.html
file (apart from the input filename shown at the top).
> csv2html --mol drugs.sdf --html drugs.html
See also in OEChem TK manual
Theory
SD Tagged Data Manipulation section
CSV File Format section
API
OEGetSDDataIter function
See also in OEDepict TK manual
Theory
Molecule Depiction chapter
API
OE2DMolDisplay class
OE2DMolDisplayOptions class
OEPrepareDepiction function
OERenderMoleculeToString function