Skip to content

Custom Renderers

Truthound Data Docs can be extended through custom renderers.

Template Renderers

CustomRenderer (Base Class)

The base class for all custom renderers.

from truthound.datadocs.renderers.custom import CustomRenderer

class MyRenderer(CustomRenderer):
    def __init__(self, name: str | None = None):
        super().__init__(name=name or "MyRenderer")

    def _do_render(self, ctx, theme):
        context = self._build_context(ctx, theme)
        return f"<html><body>{context['title']}</body></html>"

StringTemplateRenderer

A string template renderer using {key} placeholders.

from truthound.datadocs.renderers.custom import StringTemplateRenderer

renderer = StringTemplateRenderer(
    template="""
    <html>
        <head><title>{title}</title></head>
        <body>
            <h1>{title}</h1>
            <p>{subtitle}</p>
        </body>
    </html>
    """,
    name="MyStringRenderer",
    safe_mode=True,  # Enable HTML escaping
)

FileTemplateRenderer

A renderer that loads templates from files.

from pathlib import Path
from truthound.datadocs.renderers.custom import FileTemplateRenderer

renderer = FileTemplateRenderer(
    template_path=Path("./templates/report.html.j2"),
    engine="auto",  # "jinja2", "string", or "auto"
    name="MyFileRenderer",
    encoding="utf-8",
)

Engine Auto-detection: - .j2, .jinja, .jinja2 extensions → Jinja2 - .html containing {{ or {% → Jinja2 - Otherwise → String formatting

CallableRenderer

Uses an arbitrary function as a renderer.

from truthound.datadocs.renderers.custom import CallableRenderer

def my_render_func(ctx, theme):
    return f"""
    <html>
        <head><title>{ctx.title}</title></head>
        <body>
            <h1>{ctx.title}</h1>
            <p>Rows: {ctx.data.metadata.get('row_count', 0)}</p>
        </body>
    </html>
    """

renderer = CallableRenderer(
    render_func=my_render_func,
    name="MyCallableRenderer",
)

Template Context

All renderers generate template context through the _build_context() method.

Default Context Variables

context = {
    "title": ctx.title,           # Report title
    "subtitle": ctx.subtitle,     # Subtitle
    "locale": ctx.locale,         # Locale
    "theme": ctx.theme,           # Theme name
    "theme_css": theme.get_css(), # Theme CSS
    "metadata": data.metadata,    # Profile metadata
    "sections": data.sections,    # Section data
    "alerts": data.alerts,        # Alert list
    "recommendations": data.recommendations,  # Recommendation list
    "charts": data.charts,        # Chart data
    "tables": data.tables,        # Table data
    "raw": data.raw,              # Raw profile data
    "options": ctx.options,       # Additional options
}

Custom Context Builder

from truthound.datadocs.renderers.custom import CustomRenderer

def my_context_builder(ctx, theme):
    return {
        "title": ctx.title,
        "quality_score": ctx.data.raw.get("quality_score", 0),
        "custom_data": "Hello World",
    }

renderer = CustomRenderer(
    name="MyRenderer",
    context_builder=my_context_builder,
)

Jinja2 Template Examples

Basic Report Template

{# templates/report.html.j2 #}
<!DOCTYPE html>
<html lang="{{ locale }}">
<head>
    <meta charset="UTF-8">
    <title>{{ title }}</title>
    <style>{{ theme_css }}</style>
</head>
<body>
    <header>
        <h1>{{ title }}</h1>
        {% if subtitle %}
        <p class="subtitle">{{ subtitle }}</p>
        {% endif %}
    </header>

    <main>
        {# Overview #}
        <section id="overview">
            <h2>Overview</h2>
            <div class="metrics">
                <div class="metric">
                    <span class="value">{{ metadata.row_count | default(0) | number }}</span>
                    <span class="label">Rows</span>
                </div>
                <div class="metric">
                    <span class="value">{{ metadata.column_count | default(0) }}</span>
                    <span class="label">Columns</span>
                </div>
            </div>
        </section>

        {# Alerts #}
        {% if alerts %}
        <section id="alerts">
            <h2>Alerts</h2>
            {% for alert in alerts %}
            <div class="alert alert-{{ alert.severity }}">
                <strong>{{ alert.title }}</strong>
                <p>{{ alert.message }}</p>
            </div>
            {% endfor %}
        </section>
        {% endif %}

        {# Recommendations #}
        {% if recommendations %}
        <section id="recommendations">
            <h2>Recommendations</h2>
            <ul>
            {% for rec in recommendations %}
                <li>{{ rec }}</li>
            {% endfor %}
            </ul>
        </section>
        {% endif %}
    </main>

    <footer>
        <p>Generated by Truthound</p>
    </footer>
</body>
</html>

Usage

from pathlib import Path
from truthound.datadocs.renderers.custom import FileTemplateRenderer
from truthound.datadocs.engine.context import ReportContext, ReportData

# Create renderer
renderer = FileTemplateRenderer(
    template_path=Path("./templates/report.html.j2"),
    engine="jinja2",
)

# Create context
ctx = ReportContext(
    title="My Report",
    subtitle="Q4 Analysis",
    locale="en",
    theme="professional",
    data=ReportData(
        metadata={"row_count": 1000, "column_count": 15},
        raw=profile_dict,
    ),
)

# Render
html = renderer.render(ctx, theme=None)

Extending Chart Renderers

Registering Custom Chart Renderers

from truthound.datadocs import (
    BaseChartRenderer,
    ChartSpec,
    ChartLibrary,
    register_chart_renderer,
)

# NOTE: ChartLibrary enum currently supports only APEXCHARTS and SVG.
# To add a new library, you must first add it to the ChartLibrary enum.
# Below is an example of registering a custom renderer:

@register_chart_renderer(ChartLibrary.APEXCHARTS)
class CustomApexChartsRenderer(BaseChartRenderer):
    """Custom ApexCharts-based chart renderer."""

    library = ChartLibrary.APEXCHARTS

    def render(self, spec: ChartSpec) -> str:
        import json

        chart_id = f"chart-{id(spec)}"
        option = {
            "title": {"text": spec.title},
            "xAxis": {"data": spec.labels},
            "yAxis": {},
            "series": [{"type": "bar", "data": spec.values}],
        }

        return f"""
        <div id="{chart_id}" style="height: {spec.height}px;"></div>
        <script>
            var chart = echarts.init(document.getElementById('{chart_id}'));
            chart.setOption({json.dumps(option)});
        </script>
        """

    def get_dependencies(self) -> list[str]:
        return ["https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"]

Usage

from truthound.datadocs import get_chart_renderer, ChartLibrary

renderer = get_chart_renderer(ChartLibrary.APEXCHARTS)
html = renderer.render(chart_spec)

Extending Section Renderers

Registering Custom Section Renderers

from truthound.datadocs import (
    BaseSectionRenderer,
    SectionSpec,
    SectionType,
    register_section_renderer,
)

# NOTE: You must use the SectionType enum.
# For custom sections, use SectionType.CUSTOM.

@register_section_renderer(SectionType.CUSTOM)
class KPIDashboardSection(BaseSectionRenderer):
    """KPI dashboard section."""

    section_type = SectionType.CUSTOM

    def render(self, spec: SectionSpec, chart_renderer, theme_config) -> str:
        metrics = spec.metrics
        return f"""
        <section class="kpi-dashboard">
            <h2>{spec.title}</h2>
            <div class="kpi-grid">
                <div class="kpi-card">
                    <span class="kpi-value">{metrics.get('quality_score', 0)}%</span>
                    <span class="kpi-label">Quality Score</span>
                </div>
                <div class="kpi-card">
                    <span class="kpi-value">{metrics.get('completeness', 0):.1f}%</span>
                    <span class="kpi-label">Completeness</span>
                </div>
                <div class="kpi-card">
                    <span class="kpi-value">{metrics.get('uniqueness', 0):.1f}%</span>
                    <span class="kpi-label">Uniqueness</span>
                </div>
            </div>
        </section>
        """

Using in ReportConfig

from truthound.datadocs import ReportConfig, SectionType

config = ReportConfig(
    sections=[
        "kpi_dashboard",       # Custom section (string)
        SectionType.OVERVIEW,
        SectionType.COLUMNS,
    ]
)

Renderer Registry

Querying Registered Renderers

from truthound.datadocs import renderer_registry

# List registered chart renderers
chart_renderers = renderer_registry.list_chart_renderers()
# ['apexcharts', 'svg', 'echarts']

# List registered section renderers
section_renderers = renderer_registry.list_section_renderers()
# ['overview', 'columns', 'quality', ..., 'kpi_dashboard']

Getting Renderers

from truthound.datadocs import (
    get_chart_renderer,
    get_section_renderer,
)

# Get renderer by name
chart_renderer = get_chart_renderer("apexcharts")
section_renderer = get_section_renderer("overview")

Unregistering Renderers

from truthound.datadocs import renderer_registry

# Unregister chart renderer
renderer_registry.unregister_chart("echarts")

# Unregister section renderer
renderer_registry.unregister_section("kpi_dashboard")

API Reference

BaseRenderer

class BaseRenderer(ABC):
    def __init__(self, name: str | None = None) -> None:
        self._name = name or self.__class__.__name__

    @property
    def name(self) -> str:
        return self._name

    def render(self, ctx: "ReportContext", theme: "Theme | None") -> str:
        return self._do_render(ctx, theme)

    @abstractmethod
    def _do_render(self, ctx: "ReportContext", theme: "Theme | None") -> str:
        ...

CustomRenderer

class CustomRenderer(BaseRenderer):
    def __init__(
        self,
        name: str | None = None,
        context_builder: Callable[[ReportContext, Theme | None], dict[str, Any]] | None = None,
    ) -> None:
        ...

    def _build_context(self, ctx: ReportContext, theme: Theme | None) -> dict[str, Any]:
        ...

StringTemplateRenderer

class StringTemplateRenderer(CustomRenderer):
    def __init__(
        self,
        template: str,
        name: str | None = None,
        safe_mode: bool = True,  # HTML escaping
    ) -> None:
        ...

FileTemplateRenderer

class FileTemplateRenderer(CustomRenderer):
    def __init__(
        self,
        template_path: Path | str,
        engine: str = "auto",  # "jinja2", "string", "auto"
        name: str | None = None,
        encoding: str = "utf-8",
    ) -> None:
        ...

CallableRenderer

class CallableRenderer(CustomRenderer):
    def __init__(
        self,
        render_func: Callable[[ReportContext, Theme | None], str],
        name: str | None = None,
    ) -> None:
        ...

See Also