Skip to content

render

Static rendering pipeline: renders CSV tables and matplotlib figures directly from a RecordStore.

Design note: every record type sharing a tag (Point2D/Scatter2D/Trace2D/AxLine/ HistogramEntry) is drawn onto one shared SingleAxisFigure and saved once to <tag>.jpg, since histogram data isn't special-cased onto its own figure/file; a tag mixing record types renders the same way any other tag does.

Rendering only ever reads, so worker processes each open their own read-only RecordStore connection and render disjoint tags in parallel with zero write-lock contention, since SQLite's WAL mode allows any number of concurrent readers, unlike generate_records, where only one process can ever write at a time.

render_assets

Python
render_assets(
    db_path: Path,
    output_dir: Path,
    n_procs: int = 1,
    on_progress: ProgressCallback | None = None,
) -> None

Renders CSV tables and matplotlib figures for every tag in the RecordStore at db_path, writing them under output_dir (nested per tag).

Parameters:

Name Type Description Default
db_path Path

path to the trendify output directory's .db file

required
output_dir Path

directory tables/figures are written under

required
n_procs int

number of worker processes rendering tags in parallel. n_procs == 1 runs sequentially in this process (easier to debug with full tracebacks). n_procs > 1 uses a ProcessPoolExecutor, with one read-only RecordStore connection opened per worker, which is safe and contention-free since rendering never writes.

1
on_progress ProgressCallback | None

called once per tag finished, always from this process -- never a worker, so it never needs to be picklable. Left to raise: a failing callback aborts rendering rather than being silently swallowed.

None
Source code in src/trendify/generator/render.py
Python
def render_assets(
    db_path: Path,
    output_dir: Path,
    n_procs: int = 1,
    on_progress: ProgressCallback | None = None,
) -> None:
    """
    Renders CSV tables and matplotlib figures for every tag in the `RecordStore` at
    `db_path`, writing them under `output_dir` (nested per tag).

    Args:
        db_path (Path): path to the trendify output directory's `.db` file
        output_dir (Path): directory tables/figures are written under
        n_procs (int): number of worker processes rendering tags in parallel. `n_procs == 1` runs sequentially in this process (easier to debug with full tracebacks). `n_procs > 1` uses a `ProcessPoolExecutor`, with one read-only `RecordStore` connection opened per worker, which is safe and contention-free since rendering never writes.
        on_progress (ProgressCallback | None): called once per tag finished, always from this
            process -- never a worker, so it never needs to be picklable. Left to raise: a
            failing callback aborts rendering rather than being silently swallowed.

    """
    db_path = Path(db_path)
    output_dir = Path(output_dir)

    with RecordStore.open(db_path, readonly=True) as store:
        tags = store.tag_tree()
    total_tags = len(tags)

    def _report(tag: Tag, completed: int) -> None:
        if on_progress is not None:
            on_progress(
                ProgressEvent(
                    stage="render",
                    completed=completed,
                    total=total_tags,
                    detail=str(tag),
                )
            )

    if n_procs > 1:
        root_logger = logging.getLogger()
        log_queue, listener = create_queue_listener(*root_logger.handlers)
        listener.start()
        try:
            with ProcessPoolExecutor(
                max_workers=n_procs,
                initializer=_init_worker_with_logging,
                initargs=(str(db_path), log_queue, root_logger.level),
            ) as executor:
                futures = [
                    executor.submit(
                        _render_tag,
                        tag,
                        str(output_dir),
                    )
                    for tag in tags
                ]
                for completed, future in enumerate(as_completed(futures), start=1):
                    finished_tag = future.result()
                    _report(finished_tag, completed)
        finally:
            listener.stop()
    else:
        with RecordStore.open(db_path, readonly=True) as store:
            for completed, tag in enumerate(tags, start=1):
                _render_tag_assets(store, tag, output_dir)
                _report(tag, completed)