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.

../_images/csv2html-screenshot-small.png

Figure 1. Example of depicting CSV in HTML (The screenshot is reduced here for visualization convenience)

Ingredients

Difficulty Level

🌶️ 🌶️

Download

Download code

csv2html.py

See also Usage subsection.

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

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
../_images/csv2html-help.svg

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

API

See also in OEDepict TK manual

Theory

API