Skip to content

Render Stage

The render stage transforms compiled boards with data into visual output formats.

CompiledBoard + Executor → render() → SVG | HTML | PNG | PDF | Terminal

Supported Formats

The renderer supports the following output formats:

  • SVG: Scalable vector graphics (default)
  • HTML: Interactive HTML pages with embedded charts
  • PNG: Raster image format
  • PDF: PDF documents
  • Terminal: Terminal output with ASCII/Unicode charts (prints to stdout)

Entry Points

render

The main entry point for rendering dashboards:

render

render(board: CompiledBoard, executor: Executor, format: str = 'svg', variables: Optional[VariableValues] = None, **options: Any) -> Union[str, bytes]

Render a compiled dashboard.

Stage: RENDER (Main Entry Point)

This is the main rendering function. It walks the layout structure, renders each chart (triggering lazy query execution), and produces output in the requested format.

PARAMETER DESCRIPTION
board

Compiled dashboard to render

TYPE: CompiledBoard

executor

Executor for query execution

TYPE: Executor

format

Output format (svg, html, png, pdf, terminal)

TYPE: str DEFAULT: 'svg'

variables

Variable values for queries

TYPE: Optional[VariableValues] DEFAULT: None

**options

Format-specific options - background: Background color - scale: Scale factor (for png)

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Union[str, bytes]

Rendered output: - str for svg, html, terminal - bytes for png, pdf

RAISES DESCRIPTION
RenderError

If rendering fails

FormatError

If format is unknown

Source code in dataface/render/renderer.py
def render(
    board: CompiledBoard,
    executor: Executor,
    format: str = "svg",
    variables: Optional[VariableValues] = None,
    **options: Any,
) -> Union[str, bytes]:
    """Render a compiled dashboard.

    Stage: RENDER (Main Entry Point)

    This is the main rendering function. It walks the layout structure,
    renders each chart (triggering lazy query execution), and produces
    output in the requested format.

    Args:
        board: Compiled dashboard to render
        executor: Executor for query execution
        format: Output format (svg, html, png, pdf, terminal)
        variables: Variable values for queries
        **options: Format-specific options
            - background: Background color
            - scale: Scale factor (for png)

    Returns:
        Rendered output:
            - str for svg, html, terminal
            - bytes for png, pdf

    Raises:
        RenderError: If rendering fails
        FormatError: If format is unknown
    """
    # Build variable registry from board tree (for template resolution)
    from dataface.render.variable_registry import (
        build_variable_registry_from_board,
        build_variable_types_from_registry,
        build_variable_defaults_from_registry,
    )

    variable_registry = build_variable_registry_from_board(board)
    variable_types = build_variable_types_from_registry(variable_registry)
    variable_defaults = build_variable_defaults_from_registry(variable_registry)

    # Merge variables with defaults
    # Start with all variables from global registry (set to None if no default)
    # This ensures all variables are in context for template resolution
    all_variables: Dict[str, Any] = {}
    for var_name in variable_types.keys():
        all_variables[var_name] = None
    # Override with defaults
    all_variables.update(variable_defaults)
    # Override with provided values
    # Parse JSON strings in variables (from URL parameters)
    parsed_variables = _parse_variable_json_strings(variables or {})

    # Validate variables against their expected types
    validated_variables, validation_errors = _validate_variables(
        parsed_variables, variable_types
    )
    if validation_errors:
        # Log validation errors but don't fail rendering
        import warnings

        for error in validation_errors:
            warnings.warn(f"Variable validation: {error}", UserWarning)

    merged_variables = {**all_variables, **validated_variables}

    # Get background for format
    background = get_background_for_format(
        format,
        board.style,
        options.get("background"),
    )

    # Render layout to SVG
    # Format determines whether variables are interactive (foreignObject) or read-only (static text)
    # PNG/PDF export requires read-only because svglib doesn't support foreignObject
    # HTML format supports foreignObject, so it should also get interactive variables
    interactive = format in ("svg", "html")

    try:
        grid_enabled = options.get("grid", False)
        svg_content = _render_board_svg(
            board, executor, merged_variables, background, grid_enabled, interactive
        )
    except Exception as e:
        raise RenderError(f"Failed to render dashboard: {e}")

    # Convert to requested format
    if format == "svg":
        return svg_content

    elif format == "html":
        # SVG-First Migration: HTML format is now a thin wrapper around SVG
        # The SVG content already contains all interactivity via foreignObject + embedded JS
        return _to_html(board, svg_content, background, executor, merged_variables)

    elif format == "png":
        return _to_png(svg_content, options.get("scale", 1.0))

    elif format == "pdf":
        return _to_pdf(svg_content)

    elif format == "terminal":
        return _to_terminal(board, executor, merged_variables, **options)

    else:
        raise FormatError(f"Unknown format: {format}", format)

render_chart

Render a single chart:

render_chart

render_chart(chart: CompiledChart, data: List[Dict[str, Any]], **options: Any) -> str

Render a single chart to SVG.

PARAMETER DESCRIPTION
chart

CompiledChart to render

TYPE: CompiledChart

data

Query results for the chart

TYPE: List[Dict[str, Any]]

**options

Rendering options

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
str

SVG string for the chart

Source code in dataface/render/renderer.py
def render_chart(
    chart: CompiledChart,
    data: List[Dict[str, Any]],
    **options: Any,
) -> str:
    """Render a single chart to SVG.

    Args:
        chart: CompiledChart to render
        data: Query results for the chart
        **options: Rendering options

    Returns:
        SVG string for the chart
    """
    from dataface.render.vega_lite import render_chart as vega_render

    return vega_render(chart, data, format="svg")

Layout Functions

The layout module provides functions for rendering different layout types.

Rows Layout

render_rows_layout_svg

render_rows_layout_svg(items: List[LayoutItem], executor: Executor, variables: VariableValues, available_width: float, available_height: float, gap: float, background: Optional[str] = None, theme: Optional[str] = None) -> str

Render items in vertical stack as SVG.

In a rows layout, items stack vertically. Heights are determined by: 1. Pre-calculated dimensions from sizing module (preferred) 2. Content-aware fallback if not pre-calculated

PARAMETER DESCRIPTION
items

Layout items to render

TYPE: List[LayoutItem]

executor

Executor for query execution

TYPE: Executor

variables

Variable values for queries

TYPE: VariableValues

available_width

Available container width

TYPE: float

available_height

Available container height

TYPE: float

gap

Gap between items

TYPE: float

background

Optional background color

TYPE: Optional[str] DEFAULT: None

theme

Optional theme name to apply

TYPE: Optional[str] DEFAULT: None

RETURNS DESCRIPTION
str

SVG string for rows layout

Source code in dataface/render/layouts.py
def render_rows_layout_svg(
    items: List[LayoutItem],
    executor: Executor,
    variables: VariableValues,
    available_width: float,
    available_height: float,
    gap: float,
    background: Optional[str] = None,
    theme: Optional[str] = None,
) -> str:
    """Render items in vertical stack as SVG.

    In a rows layout, items stack vertically. Heights are determined by:
    1. Pre-calculated dimensions from sizing module (preferred)
    2. Content-aware fallback if not pre-calculated

    Args:
        items: Layout items to render
        executor: Executor for query execution
        variables: Variable values for queries
        available_width: Available container width
        available_height: Available container height
        gap: Gap between items
        background: Optional background color
        theme: Optional theme name to apply

    Returns:
        SVG string for rows layout
    """
    from dataface.render.renderer import _render_layout_item
    from dataface.compile.sizing import _get_item_content_height
    import re

    if not items:
        return f'<svg width="{available_width}" height="{available_height}" viewBox="0 0 {available_width} {available_height}"></svg>'

    rendered_items: List[str] = []
    current_y = 0.0

    for item in items:
        # Use pre-calculated height if available, otherwise use content-aware fallback
        if item.height > 0:
            item_height = item.height
        else:
            item_height = _get_item_content_height(item, gap, available_width)

        item_width = item.width if item.width > 0 else available_width

        item_svg = _render_layout_item(
            item, executor, variables, item_width, item_height, theme
        )

        if item_svg:
            # Extract actual rendered height from SVG
            actual_height = item_height
            height_match = re.search(r'height="([0-9.]+)"', item_svg)
            if height_match:
                actual_height = float(height_match.group(1))

            rendered_items.append(
                f'<g transform="translate(0, {current_y})">{item_svg}</g>'
            )
            current_y += actual_height + gap

    total_height = max(current_y - gap, 100.0)  # Remove last gap

    bg_rect = ""
    if background:
        bg_rect = f'<rect x="0" y="0" width="{available_width}" height="{total_height}" fill="{html.escape(background)}" rx="4"/>'

    return f"""<svg width="{available_width}" height="{total_height}" viewBox="0 0 {available_width} {total_height}">
{bg_rect}
{"".join(rendered_items)}
</svg>"""

Columns Layout

render_cols_layout_svg

render_cols_layout_svg(items: List[LayoutItem], executor: Executor, variables: VariableValues, available_width: float, available_height: float, gap: float, background: Optional[str] = None, theme: Optional[str] = None) -> str

Render items in horizontal distribution as SVG.

Trusts the normalizer for all sizing. Uses pre-calculated item.x, item.width, and item.height values.

PARAMETER DESCRIPTION
items

Layout items to render (with pre-calculated dimensions from normalizer)

TYPE: List[LayoutItem]

executor

Executor for query execution

TYPE: Executor

variables

Variable values for queries

TYPE: VariableValues

available_width

Available container width

TYPE: float

available_height

Available container height (upper bound)

TYPE: float

gap

Gap between items (unused - normalizer already applied it)

TYPE: float

background

Optional background color

TYPE: Optional[str] DEFAULT: None

theme

Optional theme name to apply

TYPE: Optional[str] DEFAULT: None

RETURNS DESCRIPTION
str

SVG string for cols layout

Source code in dataface/render/layouts.py
def render_cols_layout_svg(
    items: List[LayoutItem],
    executor: Executor,
    variables: VariableValues,
    available_width: float,
    available_height: float,
    gap: float,
    background: Optional[str] = None,
    theme: Optional[str] = None,
) -> str:
    """Render items in horizontal distribution as SVG.

    Trusts the normalizer for all sizing. Uses pre-calculated item.x, item.width,
    and item.height values.

    Args:
        items: Layout items to render (with pre-calculated dimensions from normalizer)
        executor: Executor for query execution
        variables: Variable values for queries
        available_width: Available container width
        available_height: Available container height (upper bound)
        gap: Gap between items (unused - normalizer already applied it)
        background: Optional background color
        theme: Optional theme name to apply

    Returns:
        SVG string for cols layout
    """
    from dataface.render.renderer import _render_layout_item

    if not items:
        return f'<svg width="{available_width}" height="{available_height}" viewBox="0 0 {available_width} {available_height}"></svg>'

    # Render items using pre-calculated positions from normalizer
    rendered_items: List[str] = []
    max_height = 0.0

    for item in items:
        # Trust normalizer for dimensions
        item_w = item.width if item.width > 0 else available_width
        item_h = item.height if item.height > 0 else available_height
        x_pos = item.x  # Use pre-calculated x position from normalizer

        item_svg = _render_layout_item(item, executor, variables, item_w, item_h, theme)

        if item_svg:
            rendered_items.append(
                f'<g transform="translate({x_pos}, 0)">{item_svg}</g>'
            )
            max_height = max(max_height, item_h)

    row_height = max_height if max_height > 0 else available_height

    bg_rect = ""
    if background:
        bg_rect = f'<rect x="0" y="0" width="{available_width}" height="{row_height}" fill="{html.escape(background)}" rx="4"/>'

    return f"""<svg width="{available_width}" height="{row_height}" viewBox="0 0 {available_width} {row_height}">
{bg_rect}
{"".join(rendered_items)}
</svg>"""

Grid Layout

render_grid_layout_svg

render_grid_layout_svg(items: List[LayoutItem], executor: Executor, variables: VariableValues, available_width: float, available_height: float, columns: int, gap: float, background: Optional[str] = None, theme: Optional[str] = None) -> str

Render items in positioned grid as SVG.

Grid items have explicit x, y positions and width, height spans. Each item's dimensions are calculated based on the grid columns/rows.

PARAMETER DESCRIPTION
items

Layout items with grid positions (x, y, width, height)

TYPE: List[LayoutItem]

executor

Executor for query execution

TYPE: Executor

variables

Variable values for queries

TYPE: VariableValues

available_width

Available container width

TYPE: float

available_height

Available container height

TYPE: float

columns

Number of grid columns

TYPE: int

gap

Gap between grid cells

TYPE: float

background

Optional background color

TYPE: Optional[str] DEFAULT: None

theme

Optional theme name to apply

TYPE: Optional[str] DEFAULT: None

RETURNS DESCRIPTION
str

SVG string for grid layout

Source code in dataface/render/layouts.py
def render_grid_layout_svg(
    items: List[LayoutItem],
    executor: Executor,
    variables: VariableValues,
    available_width: float,
    available_height: float,
    columns: int,
    gap: float,
    background: Optional[str] = None,
    theme: Optional[str] = None,
) -> str:
    """Render items in positioned grid as SVG.

    Grid items have explicit x, y positions and width, height spans.
    Each item's dimensions are calculated based on the grid columns/rows.

    Args:
        items: Layout items with grid positions (x, y, width, height)
        executor: Executor for query execution
        variables: Variable values for queries
        available_width: Available container width
        available_height: Available container height
        columns: Number of grid columns
        gap: Gap between grid cells
        background: Optional background color
        theme: Optional theme name to apply

    Returns:
        SVG string for grid layout
    """
    from dataface.render.renderer import _render_layout_item

    if not items:
        return f'<svg width="{available_width}" height="{available_height}" viewBox="0 0 {available_width} {available_height}"></svg>'

    # Calculate column width
    total_gap_x = gap * (columns - 1)
    col_width = (available_width - total_gap_x) / columns

    # Calculate row height based on the number of rows in the grid
    max_row_end = 1
    for item in items:
        grid_row = item.row or 0
        grid_row_span = item.row_span or 1
        max_row_end = max(max_row_end, grid_row + grid_row_span)

    total_gap_y = gap * (max_row_end - 1)
    row_height = (available_height - total_gap_y) / max_row_end

    rendered_items: List[str] = []

    for item in items:
        # Get grid position and span
        grid_col = item.col or 0
        grid_row = item.row or 0
        grid_col_span = item.col_span or 1
        grid_row_span = item.row_span or 1

        # Use pre-calculated pixel position if available, otherwise calculate
        pixel_x = item.x if item.x > 0 else (grid_col * col_width) + (grid_col * gap)
        pixel_y = item.y if item.y > 0 else (grid_row * row_height) + (grid_row * gap)

        # Use pre-calculated dimensions if available, otherwise calculate
        item_w = (
            item.width
            if item.width > 0
            else (col_width * grid_col_span) + (gap * (grid_col_span - 1))
        )
        item_h = (
            item.height
            if item.height > 0
            else (row_height * grid_row_span) + (gap * (grid_row_span - 1))
        )

        item_svg = _render_layout_item(item, executor, variables, item_w, item_h, theme)

        if item_svg:
            rendered_items.append(
                f'<g transform="translate({pixel_x}, {pixel_y})">{item_svg}</g>'
            )

    bg_rect = ""
    if background:
        bg_rect = f'<rect x="0" y="0" width="{available_width}" height="{available_height}" fill="{html.escape(background)}" rx="4"/>'

    return f"""<svg width="{available_width}" height="{available_height}" viewBox="0 0 {available_width} {available_height}">
{bg_rect}
{"".join(rendered_items)}
</svg>"""

Tabs Layout

render_tabs_layout_svg

render_tabs_layout_svg(items: List[LayoutItem], executor: Executor, variables: VariableValues, available_width: float, available_height: float, tab_titles: Optional[List[str]] = None, active_tab: int = 0, tab_position: str = 'top', background: Optional[str] = None, theme: Optional[str] = None) -> str

Render tabbed container as SVG (active tab only).

In a tabs layout, each tab gets the full container size minus the tab bar. Only the active tab is rendered in SVG output.

PARAMETER DESCRIPTION
items

Layout items (one per tab)

TYPE: List[LayoutItem]

executor

Executor for query execution

TYPE: Executor

variables

Variable values for queries

TYPE: VariableValues

available_width

Available container width

TYPE: float

available_height

Available container height

TYPE: float

tab_titles

Optional titles for tabs

TYPE: Optional[List[str]] DEFAULT: None

active_tab

Index of active tab (0-based)

TYPE: int DEFAULT: 0

tab_position

Position of tabs ("top" or "left")

TYPE: str DEFAULT: 'top'

background

Optional background color

TYPE: Optional[str] DEFAULT: None

theme

Optional theme name to apply

TYPE: Optional[str] DEFAULT: None

RETURNS DESCRIPTION
str

SVG string for tabs layout (showing only active tab)

Source code in dataface/render/layouts.py
def render_tabs_layout_svg(
    items: List[LayoutItem],
    executor: Executor,
    variables: VariableValues,
    available_width: float,
    available_height: float,
    tab_titles: Optional[List[str]] = None,
    active_tab: int = 0,
    tab_position: str = "top",
    background: Optional[str] = None,
    theme: Optional[str] = None,
) -> str:
    """Render tabbed container as SVG (active tab only).

    In a tabs layout, each tab gets the full container size minus the tab bar.
    Only the active tab is rendered in SVG output.

    Args:
        items: Layout items (one per tab)
        executor: Executor for query execution
        variables: Variable values for queries
        available_width: Available container width
        available_height: Available container height
        tab_titles: Optional titles for tabs
        active_tab: Index of active tab (0-based)
        tab_position: Position of tabs ("top" or "left")
        background: Optional background color
        theme: Optional theme name to apply

    Returns:
        SVG string for tabs layout (showing only active tab)
    """
    from dataface.render.renderer import _render_layout_item

    if not items:
        return f'<svg width="{available_width}" height="{available_height}" viewBox="0 0 {available_width} {available_height}"></svg>'

    # Render only active tab
    tab_bar_height = 40.0
    content_height = (
        available_height - tab_bar_height if tab_position == "top" else available_height
    )
    content_y = tab_bar_height if tab_position == "top" else 0.0

    active_item = items[min(active_tab, len(items) - 1)]
    item_svg = _render_layout_item(
        active_item, executor, variables, available_width, content_height, theme
    )

    # Render tab bar
    tab_bar_svg = ""
    if tab_titles and len(tab_titles) == len(items):
        tab_width = available_width / len(items)
        for idx, title in enumerate(tab_titles):
            x_pos = idx * tab_width
            is_active = idx == active_tab
            fill_color = "#e0e0e0" if is_active else "#f5f5f5"
            text_color = "#1a1a1a" if is_active else "#666666"
            escaped_title = html.escape(title)

            tab_bar_svg += f"""
            <rect x="{x_pos}" y="0" width="{tab_width}" height="{tab_bar_height}" fill="{fill_color}" stroke="#d0d0d0" stroke-width="1"/>
            <text x="{x_pos + tab_width / 2}" y="{tab_bar_height / 2 + 5}" text-anchor="middle" font-size="14" fill="{text_color}" font-weight="{'600' if is_active else '400'}">{escaped_title}</text>
            """
    else:
        # Generate default tab titles
        tab_width = available_width / len(items)
        for idx in range(len(items)):
            x_pos = idx * tab_width
            is_active = idx == active_tab
            fill_color = "#e0e0e0" if is_active else "#f5f5f5"
            text_color = "#1a1a1a" if is_active else "#666666"
            tab_title = f"Tab {idx + 1}"

            tab_bar_svg += f"""
            <rect x="{x_pos}" y="0" width="{tab_width}" height="{tab_bar_height}" fill="{fill_color}" stroke="#d0d0d0" stroke-width="1"/>
            <text x="{x_pos + tab_width / 2}" y="{tab_bar_height / 2 + 5}" text-anchor="middle" font-size="14" fill="{text_color}" font-weight="{'600' if is_active else '400'}">{tab_title}</text>
            """

    content_svg = ""
    if item_svg:
        content_svg = f'<g transform="translate(0, {content_y})">{item_svg}</g>'

    bg_rect = ""
    if background:
        bg_rect = f'<rect x="0" y="0" width="{available_width}" height="{available_height}" fill="{html.escape(background)}" rx="4"/>'

    return f"""<svg width="{available_width}" height="{available_height}" viewBox="0 0 {available_width} {available_height}">
{bg_rect}
{tab_bar_svg}
{content_svg}
</svg>"""

Vega-Lite Integration

Charts are rendered using Vega-Lite specifications.

generate_vega_lite_spec

generate_vega_lite_spec

generate_vega_lite_spec(chart: Union[CompiledChart, Any], data: List[Dict[str, Any]], width: Optional[float] = None, height: Optional[float] = None, theme: Optional[str] = None) -> Dict[str, Any]

Generate a Vega-Lite specification from a chart definition and data.

PARAMETER DESCRIPTION
chart

CompiledChart definition (or legacy Chart type)

TYPE: Union[CompiledChart, Any]

data

List of dicts containing chart data

TYPE: List[Dict[str, Any]]

width

Optional explicit width in pixels

TYPE: Optional[float] DEFAULT: None

height

Optional explicit height in pixels

TYPE: Optional[float] DEFAULT: None

theme

Optional Vega-Lite theme name to apply

TYPE: Optional[str] DEFAULT: None

RETURNS DESCRIPTION
Dict[str, Any]

Vega-Lite specification dictionary

Source code in dataface/render/vega_lite.py
def generate_vega_lite_spec(
    chart: Union[CompiledChart, Any],
    data: List[Dict[str, Any]],
    width: Optional[float] = None,
    height: Optional[float] = None,
    theme: Optional[str] = None,
) -> Dict[str, Any]:
    """Generate a Vega-Lite specification from a chart definition and data.

    Args:
        chart: CompiledChart definition (or legacy Chart type)
        data: List of dicts containing chart data
        width: Optional explicit width in pixels
        height: Optional explicit height in pixels
        theme: Optional Vega-Lite theme name to apply

    Returns:
        Vega-Lite specification dictionary
    """
    if not isinstance(data, list):
        data = list(data) if data else []

    data = _normalize_data_types(data)

    spec: Dict[str, Any] = {
        "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
        "background": None,
        "data": {"values": data},
    }

    # Apply explicit dimensions if provided
    # Use "fit" autosize mode to fill container while respecting dimensions
    if width is not None and width > 0:
        # Use full width - Vega-Lite will handle padding for axis labels
        spec["width"] = width

    if height is not None and height > 0:
        # Use full height - Vega-Lite will handle padding for title and axis labels
        spec["height"] = height

    # Use "fit" autosize with "contains": "padding" to fit chart within
    # specified dimensions while including space for axis labels and legends
    spec["autosize"] = {"type": "fit", "contains": "padding"}

    chart_type_map: Dict[str, str] = {
        # Direct mappings
        "bar": "bar",
        "line": "line",
        "area": "area",
        "point": "point",
        "circle": "circle",
        "square": "square",
        "text": "text",
        "tick": "tick",
        "rule": "rule",
        "trail": "trail",
        "rect": "rect",
        "arc": "arc",
        # Composite marks
        "boxplot": "boxplot",
        "errorbar": "errorbar",
        "errorband": "errorband",
        # Special marks
        "geoshape": "geoshape",
        "image": "image",
        # Map types (handled by _generate_map_spec and _generate_point_map_spec)
        "map": "geoshape",
        "choropleth": "geoshape",
        "point_map": "circle",
        "bubble_map": "circle",
        # Aliases
        "scatter": "point",
        "heatmap": "rect",
        "pie": "arc",
        "donut": "arc",
        "histogram": "bar",
        # Dataface-specific
        # Note: "table" is NOT here - tables use render_table_svg(), not Vega-Lite
        "kpi": "text",
    }

    chart_type = getattr(chart, "type", "bar")
    vega_type = chart_type_map.get(chart_type, "bar")

    # Handle special chart types that need custom generation
    # Note: Tables are handled by render_table_svg(), not here.
    if chart_type == "kpi":
        spec = _generate_kpi_spec(chart, data, width=width, height=height)
        if theme:
            spec = _apply_theme_to_spec(spec, theme)
        return spec
    elif chart_type == "histogram":
        spec = _generate_histogram_spec(chart, data, spec, width, height)
        if theme:
            spec = _apply_theme_to_spec(spec, theme)
        return spec
    elif chart_type == "boxplot":
        spec = _generate_boxplot_spec(chart, data, spec)
        if theme:
            spec = _apply_theme_to_spec(spec, theme)
        return spec
    elif chart_type in ("errorbar", "errorband"):
        spec = _generate_error_spec(chart, data, spec, chart_type)
        if theme:
            spec = _apply_theme_to_spec(spec, theme)
        return spec
    elif chart_type in ("arc", "pie", "donut"):
        spec = _generate_arc_spec(chart, data, spec, chart_type, width, height)
        if theme:
            spec = _apply_theme_to_spec(spec, theme)
        return spec
    elif chart_type in ("rect", "square", "heatmap"):
        spec = _generate_rect_spec(chart, data, spec, chart_type, width, height)
        if theme:
            spec = _apply_theme_to_spec(spec, theme)
        return spec
    elif chart_type in ("map", "choropleth", "geoshape"):
        spec = _generate_map_spec(chart, data, spec, chart_type, width, height)
        if theme:
            spec = _apply_theme_to_spec(spec, theme)
        return spec
    elif chart_type in ("point_map", "bubble_map"):
        spec = _generate_point_map_spec(chart, data, spec, chart_type, width, height)
        if theme:
            spec = _apply_theme_to_spec(spec, theme)
        return spec

    encoding: Dict[str, Any] = {}
    columns = set(data[0].keys()) if data else set()

    x_field = getattr(chart, "x", None)
    x_label = getattr(chart, "x_label", None)
    if x_field:
        encoding["x"] = {
            "field": x_field,
            "type": (
                _infer_vega_type_from_data(data, x_field)
                if x_field in columns
                else "nominal"
            ),
            "title": x_label if x_label else slug_to_title(x_field),
            "axis": _get_smart_x_axis_config(data, x_field, width),
        }
        if chart_type in ["bar", "line", "area"]:
            encoding["x"]["sort"] = None

    y_field = getattr(chart, "y", None)
    y_label = getattr(chart, "y_label", None)
    if y_field:
        if isinstance(y_field, list):
            return _generate_layered_spec(chart, data, encoding, width, height)
        else:
            encoding["y"] = {
                "field": y_field,
                "type": "quantitative",
                "title": y_label if y_label else slug_to_title(y_field),
            }

    color_field = getattr(chart, "color", None)
    if color_field:
        encoding["color"] = {
            "field": color_field,
            "type": (
                _infer_vega_type_from_data(data, color_field)
                if color_field in columns
                else "nominal"
            ),
        }

    size_field = getattr(chart, "size", None)
    if size_field:
        encoding["size"] = {"field": size_field, "type": "quantitative"}

    shape_field = getattr(chart, "shape", None)
    if shape_field:
        encoding["shape"] = {"field": shape_field, "type": "nominal"}

    # Handle horizontal bar charts via settings.orientation
    # Swap x and y encodings so bars grow horizontally
    settings = getattr(chart, "settings", None)
    if chart_type == "bar" and settings and settings.get("orientation") == "horizontal":
        if "x" in encoding and "y" in encoding:
            # Swap the encodings
            encoding["x"], encoding["y"] = encoding["y"], encoding["x"]

            # Category axis is now Y; x-axis "smart axis" config can be harmful here
            # (e.g. labelAngle rotation makes horizontal bar labels look odd)
            if isinstance(encoding["y"].get("axis"), dict):
                encoding["y"]["axis"].pop("labelAngle", None)
                encoding["y"]["axis"].pop("labelAlign", None)
                encoding["y"]["axis"].pop("labelBaseline", None)

            # Remove sort from the quantitative axis (now X)
            if isinstance(encoding["x"], dict):
                encoding["x"].pop("sort", None)

    # For line and area charts, use interactive crosshair tooltip
    if chart_type in ["line", "area"] and x_field and y_field:
        return _generate_line_chart_with_crosshair(
            chart,
            data,
            spec,
            encoding,
            x_field,
            y_field,
            color_field,
            vega_type,
            width,
            height,
        )

    # Enable tooltips for interactive charts
    spec["mark"] = {"type": vega_type, "tooltip": True}
    spec["encoding"] = encoding

    # Add tooltip encoding with all relevant fields
    tooltip_fields = _build_tooltip_encoding(encoding, data)
    if tooltip_fields:
        spec["encoding"]["tooltip"] = tooltip_fields

    style = getattr(chart, "style", None)
    if style:
        _apply_style(spec, style)

    title = getattr(chart, "title", None)
    if title:
        spec["title"] = {"text": title, "anchor": "start", "align": "left"}

    # Apply theme if specified
    if theme:
        spec = _apply_theme_to_spec(spec, theme)

    return spec

render_chart

render_chart

render_chart(chart: Union[CompiledChart, Any], data: List[Dict[str, Any]], format: str = 'json', width: Optional[float] = None, height: Optional[float] = None, theme: Optional[str] = None) -> str

Render a chart to the specified format.

PARAMETER DESCRIPTION
chart

CompiledChart definition

TYPE: Union[CompiledChart, Any]

data

List of dicts containing chart data

TYPE: List[Dict[str, Any]]

format

Output format ('json', 'svg', 'png', 'pdf')

TYPE: str DEFAULT: 'json'

width

Optional explicit width in pixels

TYPE: Optional[float] DEFAULT: None

height

Optional explicit height in pixels

TYPE: Optional[float] DEFAULT: None

theme

Optional theme name to apply (e.g., 'dark', 'light')

TYPE: Optional[str] DEFAULT: None

RETURNS DESCRIPTION
str

Rendered chart as string

RAISES DESCRIPTION
ValueError

If format is not supported

ImportError

If vl-convert-python is not installed for non-JSON formats

Source code in dataface/render/vega_lite.py
def render_chart(
    chart: Union[CompiledChart, Any],
    data: List[Dict[str, Any]],
    format: str = "json",
    width: Optional[float] = None,
    height: Optional[float] = None,
    theme: Optional[str] = None,
) -> str:
    """Render a chart to the specified format.

    Args:
        chart: CompiledChart definition
        data: List of dicts containing chart data
        format: Output format ('json', 'svg', 'png', 'pdf')
        width: Optional explicit width in pixels
        height: Optional explicit height in pixels
        theme: Optional theme name to apply (e.g., 'dark', 'light')

    Returns:
        Rendered chart as string

    Raises:
        ValueError: If format is not supported
        ImportError: If vl-convert-python is not installed for non-JSON formats
    """
    # Handle table charts specially - they render directly to SVG
    chart_type = getattr(chart, "type", "bar")
    if chart_type == "table":
        if format == "svg":
            return render_table_svg(
                chart, data, width=width, height=height, theme=theme
            )
        elif format == "json":
            # Return a simple JSON representation for tables
            import json

            if not isinstance(data, list):
                data = list(data) if data else []
            data = _normalize_data_types(data)

            return json.dumps(
                {
                    "type": "table",
                    "title": getattr(chart, "title", None),
                    "columns": list(data[0].keys()) if data else [],
                    "data": data,
                    "width": width,
                    "height": height,
                },
                indent=2,
            )
        elif format in ["png", "pdf"]:
            # For PNG/PDF, render SVG first then convert using svglib + reportlab
            svg_content = render_table_svg(
                chart, data, width=width, height=height, theme=theme
            )
            try:
                import base64
                from io import BytesIO

                from svglib.svglib import renderSVG  # type: ignore[import-untyped]

                drawing = renderSVG.render(svg_content)
                if drawing is None:
                    raise ValueError("Failed to parse SVG content")

                if format == "png":
                    from reportlab.graphics import renderPM  # type: ignore[import-untyped]

                    # Apply 2x scale for higher resolution
                    drawing.width = drawing.width * 2
                    drawing.height = drawing.height * 2
                    drawing.scale(2, 2)

                    png_buffer = BytesIO()
                    renderPM.drawToFile(drawing, png_buffer, fmt="PNG")
                    return base64.b64encode(png_buffer.getvalue()).decode("utf-8")
                else:  # pdf
                    from reportlab.graphics import renderPDF  # type: ignore[import-untyped]

                    pdf_buffer = BytesIO()
                    renderPDF.drawToFile(drawing, pdf_buffer)
                    return base64.b64encode(pdf_buffer.getvalue()).decode("utf-8")
            except ImportError:
                raise ImportError(
                    f"svglib and reportlab are required for {format} table rendering. "
                    "Install with: pip install svglib reportlab"
                )
        else:
            raise ValueError(
                f"Unsupported format: {format}. Supported: json, svg, png, pdf"
            )

    # Standard Vega-Lite chart rendering
    spec = generate_vega_lite_spec(chart, data, width=width, height=height, theme=theme)

    if format == "json":
        import json

        return json.dumps(spec, indent=2)
    elif format in ["svg", "png", "pdf"]:
        try:
            import vl_convert as vlc
        except ImportError:
            raise ImportError(
                f"vl-convert-python is required for {format} rendering. "
                "Install with: pip install vl-convert-python"
            )

        if format == "svg":
            # TODO: OPTIMIZATION OPPORTUNITY - Batch Vega-Lite Rendering
            # Currently each chart calls vegalite_to_svg() individually.
            # Could achieve ~30% speedup by batching multiple charts into a single
            # Vega-Lite spec using vconcat/hconcat, making ONE vl-convert call,
            # then extracting individual chart SVGs from the result.
            # See: plans/features/VEGA_SVG_BATCH_CONVERSION_ANALYSIS.md
            # GitHub Issue: https://github.com/davefowler/dataface/issues/117
            return vlc.vegalite_to_svg(spec)
        elif format == "png":
            import base64

            png_bytes = vlc.vegalite_to_png(spec, scale=2)
            return base64.b64encode(png_bytes).decode("utf-8")
        else:  # pdf
            import base64

            pdf_bytes = vlc.vegalite_to_pdf(spec)
            return base64.b64encode(pdf_bytes).decode("utf-8")

    raise ValueError(f"Unsupported format: {format}. Supported: json, svg, png, pdf")

HTML Output

HTML output is now a minimal wrapper around SVG output. Use format='html' to get a complete HTML document that embeds the SVG dashboard with proper styling.

The SVG content includes all interactivity via embedded JavaScript and foreignObject elements for variable controls.


Configuration

RenderConfig

RenderConfig dataclass

RenderConfig(background: Optional[str] = None, backgrounds: Dict[str, Optional[str]] = dict(), scale: float = 1.0, quality: int = 90)

Rendering configuration.

ATTRIBUTE DESCRIPTION
background

Default background color

TYPE: Optional[str]

backgrounds

Format-specific backgrounds

TYPE: Dict[str, Optional[str]]

scale

Scale factor for PNG output

TYPE: float

quality

Quality setting for image output

TYPE: int

get_background_for_format

get_background_for_format

get_background_for_format(format: str, style: Optional[Any] = None, override: Optional[str] = None) -> Optional[str]

Get the appropriate background color for a format.

Priority: 1. Override parameter 2. Style format-specific background 3. Style general background 4. Default for format

PARAMETER DESCRIPTION
format

Output format (svg, html, png, pdf, terminal)

TYPE: str

style

Board style (BoardStyle object or dict for backwards compat)

TYPE: Optional[Any] DEFAULT: None

override

Explicit override value

TYPE: Optional[str] DEFAULT: None

RETURNS DESCRIPTION
Optional[str]

Background color or None for transparent

Source code in dataface/render/config.py
def get_background_for_format(
    format: str,
    style: Optional[Any] = None,
    override: Optional[str] = None,
) -> Optional[str]:
    """Get the appropriate background color for a format.

    Priority:
    1. Override parameter
    2. Style format-specific background
    3. Style general background
    4. Default for format

    Args:
        format: Output format (svg, html, png, pdf, terminal)
        style: Board style (BoardStyle object or dict for backwards compat)
        override: Explicit override value

    Returns:
        Background color or None for transparent
    """
    if override is not None:
        return None if override == "transparent" else override

    if style:
        # Handle both BoardStyle object and dict for backwards compatibility
        from dataface.compile.compiled_types import BoardStyle

        if isinstance(style, BoardStyle):
            # Use pre-parsed background
            bg = style.background
            if bg:
                return None if bg == "transparent" else bg
        elif isinstance(style, dict):
            # Legacy dict style - check format-specific first
            backgrounds = style.get("backgrounds", {})
            if format in backgrounds:
                bg = backgrounds[format]
                return None if bg == "transparent" else bg

            # Check general background
            if "background" in style:
                bg = style["background"]
                return None if bg == "transparent" else bg

    # Default for format
    return _default_config.backgrounds.get(format)

Errors

errors

Rendering error types.

Stage: RENDER Purpose: Define error types for rendering failures.

These errors are raised during: - Chart rendering (ChartError) - Layout rendering (LayoutError) - Format conversion (FormatError)

All errors inherit from RenderError for easy catching.

Note: Many render errors are displayed IN the output rather than thrown, so users see helpful error messages in the rendered dashboard.

RenderError

RenderError(message: str, element: Optional[str] = None)

Bases: Exception

Base error for all rendering failures.

This is the parent class for all rendering-related errors. Catch this to handle any rendering error.

ATTRIBUTE DESCRIPTION
message

Human-readable error description

element

Element that failed to render (if applicable)

Source code in dataface/render/errors.py
def __init__(self, message: str, element: Optional[str] = None):
    self.message = message
    self.element = element
    super().__init__(self._format_message())

FormatError

FormatError(message: str, format: Optional[str] = None)

Bases: RenderError

Error during format conversion.

Raised when: - Unknown format requested - SVG to PNG/PDF conversion fails - HTML template error

Example

try: ... render(board, executor, format="unknown") ... except FormatError as e: ... print(f"Format error: {e}")

Source code in dataface/render/errors.py
def __init__(self, message: str, format: Optional[str] = None):
    self.format = format
    super().__init__(f"Format conversion failed: {message}", format)

ChartError

ChartError(message: str, chart_id: Optional[str] = None)

Bases: RenderError

Error during chart rendering.

Raised when: - Chart type not supported - Data doesn't match chart requirements - Vega-Lite spec generation fails

Example

try: ... render_chart(chart, data) ... except ChartError as e: ... print(f"Chart failed: {e}")

Source code in dataface/render/errors.py
def __init__(self, message: str, chart_id: Optional[str] = None):
    self.chart_id = chart_id
    super().__init__(f"Chart rendering failed: {message}", chart_id)