Skip to content

Render Stage

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

CompiledFace + 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 datafaces:

render

render(face: CompiledFace, executor: Executor, format: str = 'svg', variables: VariableValues | None = None, **options: Any) -> RenderResult

Render a compiled dataface.

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
face

Compiled dataface to render

TYPE: CompiledFace

executor

Executor for query execution

TYPE: Executor

format

Output format (svg, html, png, pdf, terminal, json, text, yaml)

TYPE: str DEFAULT: 'svg'

variables

Variable values for queries

TYPE: VariableValues | None DEFAULT: None

**options

Format-specific options - background: Background color - scale: Scale factor (for png) - grid: Show grid overlay (for debugging)

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
RenderResult

RenderResult with: - output: rendered content (str or bytes) - chart_errors: per-chart runtime failures (face still rendered) - face_error: post-validation fatal (None when render succeeded)

RAISES DESCRIPTION
RenderError

If a face-level invariant is violated before rendering starts

FormatError

If format is unknown

Source code in dataface/core/render/renderer.py
def render(
    face: CompiledFace,
    executor: Executor,
    format: str = "svg",
    variables: VariableValues | None = None,
    **options: Any,
) -> RenderResult:
    """Render a compiled dataface.

    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:
        face: Compiled dataface to render
        executor: Executor for query execution
        format: Output format (svg, html, png, pdf, terminal, json, text, yaml)
        variables: Variable values for queries
        **options: Format-specific options
            - background: Background color
            - scale: Scale factor (for png)
            - grid: Show grid overlay (for debugging)

    Returns:
        RenderResult with:
            - output: rendered content (str or bytes)
            - chart_errors: per-chart runtime failures (face still rendered)
            - face_error: post-validation fatal (None when render succeeded)

    Raises:
        RenderError: If a face-level invariant is violated before rendering starts
        FormatError: If format is unknown
    """
    # Compile warns on orphans; render hard-fails the empty-layout-with-charts
    # case so the dashboard never silently renders with nothing visible.
    if face.charts and not face.layout.items:
        from dataface.core.errors import DF_RENDER_NO_LAYOUT

        chart_list = ", ".join(sorted(face.charts.keys()))
        raise RenderError.from_code(DF_RENDER_NO_LAYOUT, charts=chart_list)

    # Trust the normalizer - use pre-computed variable_defaults
    variable_registry = face.variable_registry or {}

    # Merge variables: start with None for all vars, then defaults, then user values
    all_variables: dict[str, Any] = dict.fromkeys(variable_registry)
    all_variables.update(face.variable_defaults)  # Pre-computed by normalizer
    # Parse JSON strings in variables (from URL parameters) and merge
    parsed_variables = parse_variable_json_strings(variables or {})
    merged_variables = {**all_variables, **parsed_variables}

    # Face-level precondition: required variables must have a value before any query runs.
    # None, empty string, and empty list all count as "not provided" — URL params arrive
    # as strings so ?var= produces "" which is as unscoped as no value at all.
    if variable_registry:

        def _is_absent(v: Any) -> bool:
            if v is None:
                return True
            if isinstance(v, str):
                return not v.strip()
            if isinstance(v, (list, tuple, set)):
                return len(v) == 0
            return False

        missing = [
            MissingVariable(
                key=key,
                label=var.label,
                description=var.description,
                input_type=var.input,
            )
            for key, var in variable_registry.items()
            if var.required is True and _is_absent(merged_variables.get(key))
        ]
        if missing:
            raise MissingRequiredVariablesError(missing)

    # Pre-render query execution — authoritative, not a fallback.
    # Queries run BEFORE calculate_data_aware_layout so the sizing pass can
    # use cached results (render-first sizing reads query data from the executor
    # cache without re-executing).
    # Phase 1: chart-direct queries without {{ results.X }} run in parallel.
    # Phase 2: chart-direct {{ results.X }} queries run sequentially once
    # their upstream results are cached.
    # Errors are stored on the executor (executor._query_errors) so that
    # execute_chart() during the render walk raises the stored error
    # instead of re-executing.  The render walk never retries a failed query.
    query_names = collect_layout_chart_query_names(face)
    parallel_query_names, sequential_query_names = plan_query_execution_phases(
        face, query_names
    )
    execute_queries_parallel(executor, face, parallel_query_names, merged_variables)
    execute_queries_sequential(executor, sequential_query_names, merged_variables)

    # Sync face.resolved_style (and all nested sub-board faces) from face.theme
    # before layout sizing so calculate_data_aware_layout reads the live value.
    sync_face_resolved_style(face)

    # Calculate layout with data awareness — table heights use actual row counts,
    # and Vega-Lite charts are rendered to get true heights (render-first sizing).
    # Render-first sizing is skipped for non-SVG formats (yaml/json/text) since
    # those formats don't render charts and don't benefit from actual heights.
    # Returns (face, render_cache) where render_cache holds pre-rendered SVGs.
    face, render_cache = calculate_data_aware_layout(
        face,
        executor,
        merged_variables,
        render_first=format in _SVG_FORMATS,
    )

    # Resolve the sized face: bake board config constants, style, and Vega config.
    # Called after data-aware sizing so resolved dimensions reflect actual heights.
    config = get_config()
    resolved_face = resolve_face(face, config)

    # Get background for format.
    #
    # Two distinct concerns:
    #   - ``background`` (the value to fill): comes from the resolved face
    #     style so the editorial-cream theme reaches the SVG outer rect even
    #     when the face YAML doesn't author its own ``background``. Falls
    #     back to format/config defaults when the resolved style has no
    #     background and no override is given.
    #   - ``bg_is_explicit`` (was the choice authored by the user?): only
    #     True for cases 1-2 (override param or a face that *literally
    #     wrote* ``style.background``). Theme-driven backgrounds stay
    #     False so the HTML converter keeps using the page background for
    #     the body. Flipping this to True for theme-only would put the
    #     working surface colour on the page canvas — wrong.
    override = options.get("background")
    bg_is_explicit = False
    authored_face_bg = face.style.background
    resolved_bg = resolved_face.style.background
    if override is not None:
        background = None if override == "transparent" else override
        bg_is_explicit = True
    elif authored_face_bg:
        background = None if authored_face_bg == "transparent" else authored_face_bg
        bg_is_explicit = True
    elif resolved_bg:
        background = None if resolved_bg == "transparent" else resolved_bg
    else:
        format_config = config.rendering.get(format)
        background = (
            format_config.background
            if format_config and format_config.background is not None
            else config.style.background
        )

    # Per-chart error collector: single append site in render_chart_item.
    # All formats share this collector so callers get chart_errors regardless of format.
    error_collector: list[StructuredError] = []

    # JSON/text formats: skip SVG rendering entirely — walk layout tree directly
    if format == "json":
        from dataface.core.render.json_format import render_face_json

        output = render_face_json(
            face, executor, merged_variables, error_collector=error_collector
        )
        return RenderResult(output=output, chart_errors=error_collector)

    if format == "text":
        from dataface.core.render.text_format import render_face_text

        output = render_face_text(
            face, executor, merged_variables, error_collector=error_collector
        )
        return RenderResult(output=output, chart_errors=error_collector)

    if format == "yaml":
        from dataface.core.render.yaml_format import render_face_yaml

        output = render_face_yaml(
            face, executor, merged_variables, error_collector=error_collector
        )
        return RenderResult(output=output, chart_errors=error_collector)

    # 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")

    # Board link rewriting: set context for the duration of this render pass
    from dataface.core.render.board_links import set_link_context

    link_context = options.get("link_context")
    set_link_context(link_context)

    try:
        grid_enabled = options.get("grid", False)
        margins_enabled = options.get("margins", False)
        svg_content = render_face_svg(
            resolved_face,
            executor,
            merged_variables,
            background,
            grid_enabled,
            interactive,
            render_cache=render_cache,
            margins=margins_enabled,
            error_collector=error_collector,
        )
    except DatafaceError as e:
        # Face-level fatal: a DF-RENDER-* code escaped chart isolation
        # (e.g. DF_RENDER_NO_LAYOUT, MissingRequiredVariablesError). Surface as
        # face_error so callers see the structured code unwrapped.
        set_link_context(None)
        return RenderResult(
            output=None,
            chart_errors=error_collector,
            face_error=e.to_structured(),
        )
    except Exception as e:  # noqa: BLE001
        from dataface.core.errors import DF_RENDER_INTERNAL

        set_link_context(None)
        wrapped = RenderError.from_code(DF_RENDER_INTERNAL, inner_message=str(e))
        return RenderResult(
            output=None,
            chart_errors=error_collector,
            face_error=wrapped.to_structured(),
        )
    finally:
        set_link_context(None)

    # Convert to requested format
    if format == "svg":
        return RenderResult(output=svg_content, chart_errors=error_collector)

    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
        # Pass background_override so the HTML converter knows whether to use
        # the theme's resolved page background or respect an explicit override.
        html_output = to_html(
            face,
            svg_content,
            background,
            executor,
            merged_variables,
            background_override=bg_is_explicit,
        )
        return RenderResult(output=html_output, chart_errors=error_collector)

    elif format == "png":
        png_scale = options.get("scale")
        if png_scale is None:
            png_scale = get_rendering_config().png.scale
        return RenderResult(
            output=to_png(svg_content, png_scale), chart_errors=error_collector
        )

    elif format == "pdf":
        return RenderResult(output=to_pdf(svg_content), chart_errors=error_collector)

    elif format == "terminal":
        return RenderResult(
            output=_to_terminal(face, executor, merged_variables, **options),
            chart_errors=error_collector,
        )

    else:
        from dataface.core.errors import DF_RENDER_FORMAT_UNSUPPORTED

        raise FormatError.from_code(DF_RENDER_FORMAT_UNSUPPORTED, format=format)

render_chart

Render a single chart:

render_chart

render_chart(chart: CompiledChart | ResolvedChart | Any, data: list[dict[str, Any]], format: str = 'json', width: float | None = None, height: float | None = None, theme: str | None = None, is_placeholder: bool = False, datasets: dict[str, list[dict[str, Any]]] | None = None, variables: dict[str, Any] | None = None, padding: dict[str, Any] | None = None, resolved_style: ResolvedStyle | None = None) -> str

Render a chart to JSON, SVG, PNG, or PDF.

Source code in dataface/core/render/chart/vega_lite.py
def render_chart(
    chart: CompiledChart | ResolvedChart | Any,
    data: list[dict[str, Any]],
    format: str = "json",
    width: float | None = None,
    height: float | None = None,
    theme: str | None = None,
    is_placeholder: bool = False,
    datasets: dict[str, list[dict[str, Any]]] | None = None,
    variables: dict[str, Any] | None = None,
    padding: dict[str, Any] | None = None,
    resolved_style: ResolvedStyle | None = None,
) -> str:
    """Render a chart to JSON, SVG, PNG, or PDF."""
    # Pass the face's resolved_style as board_style so the chart's effective
    # style reflects the active theme (axis colors, zero_color, etc.).  Without
    # this, a chart rendered with theme="editorial-cream" still gets gray axis
    # colors from the configured-default board style.
    resolved_chart = (
        chart
        if isinstance(chart, ResolvedChart)
        else resolve_chart(chart, data, board_style=resolved_style)
    )

    if format == "json":
        artifact = build_chart_json(resolved_chart, data, width=width, height=height)
        return render_chart_artifact(
            artifact,
            format,
            width=width,
            height=height,
            is_placeholder=is_placeholder,
            resolved_style=resolved_style,
        )

    render_data = data
    if is_placeholder and not data:
        from dataface.core.render.placeholder import generate_placeholder_data

        render_data = generate_placeholder_data(
            resolved_chart.chart_type, resolved_chart
        )

    artifact = renderer_registry.render(
        resolved_chart,
        render_data,
        width=width,
        height=height,
        theme=theme,
        is_placeholder=is_placeholder,
        datasets=datasets,
        variables=variables,
        padding=padding,
        resolved_style=resolved_style,
    )
    return render_chart_artifact(
        artifact,
        format,
        width=width,
        height=height,
        is_placeholder=is_placeholder,
        resolved_style=resolved_style,
    )

Layout Functions

The layout module provides functions for rendering different layout types.

Rows Layout

render_rows_layout

render_rows_layout(items: list[LayoutItem] | tuple[ResolvedLayoutItem, ...], executor: Executor, variables: VariableValues, available_width: float, available_height: float, card_gap: float, gap: float, background: str | None = None, theme: str | None = None, resolved_style: ResolvedStyle | None = None, interactive: bool = True, render_cache: dict[str, tuple[str, float]] | None = None, *, error_collector: list[StructuredError] | None = None) -> tuple[str, float]

Render items in vertical stack.

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] | tuple[ResolvedLayoutItem, ...]

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: str | None DEFAULT: None

theme

Optional theme name to apply

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
tuple[str, float]

(svg_elements_string, total_actual_height) — no wrapper.

Source code in dataface/core/render/layouts.py
def render_rows_layout(
    items: list[LayoutItem] | tuple[ResolvedLayoutItem, ...],
    executor: Executor,
    variables: VariableValues,
    available_width: float,
    available_height: float,
    card_gap: float,
    gap: float,
    background: str | None = None,
    theme: str | None = None,
    resolved_style: ResolvedStyle | None = None,
    interactive: bool = True,
    render_cache: dict[str, tuple[str, float]] | None = None,
    *,
    error_collector: list[StructuredError] | None = None,
) -> tuple[str, float]:
    """Render items in vertical stack.

    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_elements_string, total_actual_height) — no <svg> wrapper.
    """
    if not items:
        return "", 0.0

    rendered_items: list[str] = []
    current_y = 0.0
    actual_total_height = 0.0

    for item in items:
        # Use pre-calculated item dimensions for the render call.
        # Actual rendered height is read back from the item SVG.
        item_height = item.height if item.height > 0 else available_height
        item_width = item.width if item.width > 0 else available_width

        item_svg, actual_item_height = render_layout_item(
            item,
            executor,
            variables,
            card_gap=card_gap,
            available_width=item_width,
            available_height=item_height,
            gap=gap,
            theme=theme,
            resolved_style=resolved_style,
            interactive=interactive,
            render_cache=render_cache,
            error_collector=error_collector,
        )

        if item_svg:
            rendered_items.append(
                f'<g transform="translate(0, {_px(current_y)})">{item_svg}</g>'
            )
            actual_total_height = current_y + actual_item_height
            current_y += actual_item_height + gap + card_gap

    bg_rect = ""
    if background:
        bg_rect = _bg_rect(available_width, actual_total_height, background)

    return f"{bg_rect}\n{''.join(rendered_items)}", actual_total_height

Columns Layout

render_cols_layout

render_cols_layout(items: list[LayoutItem] | tuple[ResolvedLayoutItem, ...], executor: Executor, variables: VariableValues, available_width: float, available_height: float, card_gap: float, gap: float, background: str | None = None, theme: str | None = None, resolved_style: ResolvedStyle | None = None, interactive: bool = True, render_cache: dict[str, tuple[str, float]] | None = None, *, error_collector: list[StructuredError] | None = None) -> tuple[str, float]

Render items in horizontal distribution.

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] | tuple[ResolvedLayoutItem, ...]

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: str | None DEFAULT: None

theme

Optional theme name to apply

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
tuple[str, float]

(svg_elements_string, max_actual_item_height) — no wrapper.

Source code in dataface/core/render/layouts.py
def render_cols_layout(
    items: list[LayoutItem] | tuple[ResolvedLayoutItem, ...],
    executor: Executor,
    variables: VariableValues,
    available_width: float,
    available_height: float,
    card_gap: float,
    gap: float,
    background: str | None = None,
    theme: str | None = None,
    resolved_style: ResolvedStyle | None = None,
    interactive: bool = True,
    render_cache: dict[str, tuple[str, float]] | None = None,
    *,
    error_collector: list[StructuredError] | None = None,
) -> tuple[str, float]:
    """Render items in horizontal distribution.

    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_elements_string, max_actual_item_height) — no <svg> wrapper.
    """
    if not items:
        return "", 0.0

    # Render items using pre-calculated positions from normalizer
    rendered_items: list[str] = []
    max_actual_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, actual_item_height = render_layout_item(
            item,
            executor,
            variables,
            card_gap=card_gap,
            available_width=item_w,
            available_height=item_h,
            gap=gap,
            theme=theme,
            resolved_style=resolved_style,
            interactive=interactive,
            render_cache=render_cache,
            error_collector=error_collector,
        )

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

    bg_rect = ""
    if background:
        bg_rect = _bg_rect(available_width, max_actual_height, background)

    return f"{bg_rect}\n{''.join(rendered_items)}", max_actual_height

Grid Layout

render_grid_layout

render_grid_layout(items: list[LayoutItem] | tuple[ResolvedLayoutItem, ...], executor: Executor, variables: VariableValues, available_width: float, available_height: float, columns: int, card_gap: float, gap: float, background: str | None = None, theme: str | None = None, resolved_style: ResolvedStyle | None = None, interactive: bool = True, render_cache: dict[str, tuple[str, float]] | None = None, *, error_collector: list[StructuredError] | None = None) -> tuple[str, float]

Render items in positioned grid.

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] | tuple[ResolvedLayoutItem, ...]

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: str | None DEFAULT: None

theme

Optional theme name to apply

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
tuple[str, float]

(svg_elements_string, max_actual_bottom_edge) — no wrapper.

Source code in dataface/core/render/layouts.py
def render_grid_layout(
    items: list[LayoutItem] | tuple[ResolvedLayoutItem, ...],
    executor: Executor,
    variables: VariableValues,
    available_width: float,
    available_height: float,
    columns: int,
    card_gap: float,
    gap: float,
    background: str | None = None,
    theme: str | None = None,
    resolved_style: ResolvedStyle | None = None,
    interactive: bool = True,
    render_cache: dict[str, tuple[str, float]] | None = None,
    *,
    error_collector: list[StructuredError] | None = None,
) -> tuple[str, float]:
    """Render items in positioned grid.

    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_elements_string, max_actual_bottom_edge) — no <svg> wrapper.
    """
    if not items:
        return "", 0.0

    # Trust the normalizer - sizing.py calculates all grid positions and dimensions
    rendered_items: list[str] = []
    max_bottom_edge = 0.0

    for item in items:
        # Use pre-calculated pixel positions and dimensions from sizing.py
        pixel_x = item.x
        pixel_y = item.y
        item_w = item.width if item.width > 0 else available_width
        item_h = item.height if item.height > 0 else available_height

        item_svg, actual_item_height = render_layout_item(
            item,
            executor,
            variables,
            card_gap=card_gap,
            available_width=item_w,
            available_height=item_h,
            gap=gap,
            theme=theme,
            resolved_style=resolved_style,
            interactive=interactive,
            render_cache=render_cache,
            error_collector=error_collector,
        )

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

    bg_rect = ""
    if background:
        bg_rect = _bg_rect(available_width, max_bottom_edge, background)

    return f"{bg_rect}\n{''.join(rendered_items)}", max_bottom_edge

Tabs Layout

render_tabs_layout

render_tabs_layout(items: list[LayoutItem] | tuple[ResolvedLayoutItem, ...], executor: Executor, variables: VariableValues, available_width: float, available_height: float, card_gap: float = 0.0, tab_titles: list[str] | None = None, tab_slugs: list[str] | None = None, tab_variable: str | None = None, active_tab: int = 0, tab_position: str = 'top', background: str | None = None, theme: str | None = None, *, resolved_style: ResolvedStyle, interactive: bool = True, render_cache: dict[str, tuple[str, float]] | None = None, error_collector: list[StructuredError] | None = None) -> tuple[str, float]

Render tabbed container (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. The tab bar is rendered as clickable SVG links that update URL params for server re-rendering.

PARAMETER DESCRIPTION
items

Layout items (one per tab)

TYPE: list[LayoutItem] | tuple[ResolvedLayoutItem, ...]

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

Display titles for tabs

TYPE: list[str] | None DEFAULT: None

tab_slugs

URL-safe slugs for tabs (used in URL params)

TYPE: list[str] | None DEFAULT: None

tab_variable

Variable name for tab selection (URL param name)

TYPE: str | None 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: str | None DEFAULT: None

theme

Optional theme name to apply

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
tuple[str, float]

(svg_elements_string, actual_height) — no wrapper.

Source code in dataface/core/render/layouts.py
def render_tabs_layout(
    items: list[LayoutItem] | tuple[ResolvedLayoutItem, ...],
    executor: Executor,
    variables: VariableValues,
    available_width: float,
    available_height: float,
    card_gap: float = 0.0,
    tab_titles: list[str] | None = None,
    tab_slugs: list[str] | None = None,
    tab_variable: str | None = None,
    active_tab: int = 0,
    tab_position: str = "top",
    background: str | None = None,
    theme: str | None = None,
    *,
    resolved_style: ResolvedStyle,
    interactive: bool = True,
    render_cache: dict[str, tuple[str, float]] | None = None,
    error_collector: list[StructuredError] | None = None,
) -> tuple[str, float]:
    """Render tabbed container (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. The tab bar is rendered as
    clickable SVG <a href> links that update URL params for server re-rendering.

    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: Display titles for tabs
        tab_slugs: URL-safe slugs for tabs (used in URL params)
        tab_variable: Variable name for tab selection (URL param name)
        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_elements_string, actual_height) — no <svg> wrapper.
    """
    if not items:
        return "", 0.0

    # Resolve active tab from variable value (URL param overrides default)
    if tab_variable and tab_slugs:
        var_value = variables.get(tab_variable)
        if var_value and str(var_value) in tab_slugs:
            active_tab = tab_slugs.index(str(var_value))

    tabs_config = resolved_style.layout.tabs
    tab_bar_height = tabs_config.bar_height

    # Render only active tab
    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, actual_item_height = render_layout_item(
        active_item,
        executor,
        variables,
        card_gap=0.0,
        available_width=available_width,
        available_height=content_height,
        theme=theme,
        resolved_style=resolved_style,
        interactive=interactive,
        render_cache=render_cache,
        error_collector=error_collector,
    )

    # Compute tab titles once - use provided titles or generate defaults
    titles = (
        tab_titles
        if tab_titles and len(tab_titles) == len(items)
        else [f"Tab {idx + 1}" for idx in range(len(items))]
    )
    slugs = tab_slugs or [f"tab_{idx}" for idx in range(len(items))]

    # Render tab bar as clickable SVG links
    tab_width = available_width / len(titles)
    tab_bar_parts: list[str] = []
    colors = get_theme_colors(theme)
    # header_background / row_stripe are optional in the universal default
    # ("header rule only"). When the theme doesn't define them, fall back
    # to the page background so the tab chrome still reads as a flat band
    # rather than emitting fill="" (which browsers treat as invalid →
    # initial value = black). Mirrors the same fallback in geo.py.
    _active_fill = colors["header_background"] or colors["background"]
    _inactive_fill = colors["row_stripe"] or colors["background"]
    for idx, (title, slug) in enumerate(zip(titles, slugs, strict=True)):
        is_active = idx == active_tab
        x = idx * tab_width
        weight = tabs_config.active_weight if is_active else tabs_config.inactive_weight

        tab_svg = (
            f'<rect x="{x}" y="0" width="{tab_width}" height="{tab_bar_height}" '
            f'fill="{_active_fill if is_active else _inactive_fill}" '
            f'stroke="{colors["border"]}" stroke-width="{tabs_config.border.width}"/>'
            f'<text x="{x + tab_width / 2}" y="{tab_bar_height / 2 + tabs_config.title_baseline_offset}" '
            f'text-anchor="middle" font-size="{tabs_config.font.size}" fill="{colors["title_color"] if is_active else colors["muted"]}" '
            f'font-weight="{weight}">{html.escape(title)}</text>'
        )

        if tab_variable and not is_active:
            href = _build_toggle_url(variables, tab_variable, slug)
            tab_bar_parts.append(f'<a href="{html.escape(href)}">{tab_svg}</a>')
        else:
            tab_bar_parts.append(tab_svg)

    tab_bar_svg = "\n".join(tab_bar_parts)

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

    actual_height = content_y + actual_item_height
    bg_rect = ""
    if background:
        bg_rect = _bg_rect(available_width, actual_height, background)

    return f"{bg_rect}\n{tab_bar_svg}\n{content_svg}", actual_height

Vega-Lite Integration

Charts are rendered using Vega-Lite specifications.

generate_vega_lite_spec

generate_vega_lite_spec

generate_vega_lite_spec(chart: CompiledChart | ResolvedChart | Any, data: list[dict[str, Any]], width: float | None = None, height: float | None = None, theme: str | None = None) -> dict[str, Any]

Generate a Vega-Lite spec for charts that render as Vega-Lite.

Source code in dataface/core/render/chart/vega_lite.py
def generate_vega_lite_spec(
    chart: CompiledChart | ResolvedChart | Any,
    data: list[dict[str, Any]],
    width: float | None = None,
    height: float | None = None,
    theme: str | None = None,
) -> dict[str, Any]:
    """Generate a Vega-Lite spec for charts that render as Vega-Lite."""
    resolved_chart = (
        chart if isinstance(chart, ResolvedChart) else resolve_chart(chart, data)
    )
    if resolved_chart.chart_type == "table":
        return {"data": {"values": data}}
    if resolved_chart.renderer_family == "svg":
        # KPI / table / spark_bar / error are custom-SVG; they have no
        # Vega-Lite spec equivalent. Callers asking for a VL spec on these
        # types are almost always confused — fail loudly instead of returning
        # a partial spec.
        raise ValueError(
            f"Chart type '{resolved_chart.chart_type}' does not render to a Vega-Lite spec"
        )
    return render_standard_vega_spec(
        resolved_chart,
        data,
        width=width,
        height=height,
        theme=theme,
    )

render_chart

render_chart

render_chart(chart: CompiledChart | ResolvedChart | Any, data: list[dict[str, Any]], format: str = 'json', width: float | None = None, height: float | None = None, theme: str | None = None, is_placeholder: bool = False, datasets: dict[str, list[dict[str, Any]]] | None = None, variables: dict[str, Any] | None = None, padding: dict[str, Any] | None = None, resolved_style: ResolvedStyle | None = None) -> str

Render a chart to JSON, SVG, PNG, or PDF.

Source code in dataface/core/render/chart/vega_lite.py
def render_chart(
    chart: CompiledChart | ResolvedChart | Any,
    data: list[dict[str, Any]],
    format: str = "json",
    width: float | None = None,
    height: float | None = None,
    theme: str | None = None,
    is_placeholder: bool = False,
    datasets: dict[str, list[dict[str, Any]]] | None = None,
    variables: dict[str, Any] | None = None,
    padding: dict[str, Any] | None = None,
    resolved_style: ResolvedStyle | None = None,
) -> str:
    """Render a chart to JSON, SVG, PNG, or PDF."""
    # Pass the face's resolved_style as board_style so the chart's effective
    # style reflects the active theme (axis colors, zero_color, etc.).  Without
    # this, a chart rendered with theme="editorial-cream" still gets gray axis
    # colors from the configured-default board style.
    resolved_chart = (
        chart
        if isinstance(chart, ResolvedChart)
        else resolve_chart(chart, data, board_style=resolved_style)
    )

    if format == "json":
        artifact = build_chart_json(resolved_chart, data, width=width, height=height)
        return render_chart_artifact(
            artifact,
            format,
            width=width,
            height=height,
            is_placeholder=is_placeholder,
            resolved_style=resolved_style,
        )

    render_data = data
    if is_placeholder and not data:
        from dataface.core.render.placeholder import generate_placeholder_data

        render_data = generate_placeholder_data(
            resolved_chart.chart_type, resolved_chart
        )

    artifact = renderer_registry.render(
        resolved_chart,
        render_data,
        width=width,
        height=height,
        theme=theme,
        is_placeholder=is_placeholder,
        datasets=datasets,
        variables=variables,
        padding=padding,
        resolved_style=resolved_style,
    )
    return render_chart_artifact(
        artifact,
        format,
        width=width,
        height=height,
        is_placeholder=is_placeholder,
        resolved_style=resolved_style,
    )

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 dataface with proper styling.

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


Errors

errors

Rendering error types.

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

These errors are raised during: - General rendering failures (RenderError) - Format conversion (FormatError) - Pre-render required-variable validation (MissingRequiredVariablesError)

All errors inherit from RenderError → DatafaceError for easy catching.

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

RenderError

RenderError(message: str, element: str | None = None)

Bases: DatafaceError

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)

TYPE: str | None

Source code in dataface/core/render/errors.py
def __init__(self, message: str, element: str | None = None):
    self.message = message
    self.element = element
    self.fields: dict[str, Any] = {}
    super().__init__(self._format_message())
    if self.code is None:
        from dataface.core.errors.codes_unknown import DF_UNKNOWN_INTERNAL

        self.code = DF_UNKNOWN_INTERNAL

ChartDataError

ChartDataError(message: str, chart_id: str | None = None)

Bases: RenderError

Chart received data that doesn't match its requirements.

Raised when: - KPI chart receives more than 1 row - Chart references a column not present in the data - Data shape doesn't match chart type expectations

Source code in dataface/core/render/errors.py
def __init__(self, message: str, chart_id: str | None = None):
    self.chart_id = chart_id
    super().__init__(message, chart_id)

FormatError

FormatError(message: str, format: str | None = None)

Bases: RenderError

Error during format conversion.

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

Example

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

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