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¶
- HTML Reports - HTML report generation
- Charts - Chart rendering
- Sections - Section configuration
- Themes - Theme customization