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) -> str | bytes

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)

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
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/core/render/renderer.py
def render(
    face: CompiledFace,
    executor: Executor,
    format: str = "svg",
    variables: VariableValues | None = None,
    **options: Any,
) -> str | bytes:
    """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)
        variables: Variable values for queries
        **options: Format-specific options
            - background: Background color
            - scale: Scale factor (for png)
            - grid: Show grid overlay (for debugging)

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

    Raises:
        RenderError: If rendering fails
        FormatError: If format is unknown
    """
    # 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}

    # Get background for format
    # Priority: 1. Override parameter, 2. Face style, 3. Config default for format
    override = options.get("background")
    if override is not None:
        background = None if override == "transparent" else override
    elif face.style and face.style.background:
        background = (
            None if face.style.background == "transparent" else face.style.background
        )
    else:
        config = get_config()
        format_config = config.rendering.get(format)
        background = format_config.background if format_config else "#ffffff"

    # 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_face_svg(
            face, executor, merged_variables, background, grid_enabled, interactive
        )
    except Exception as e:
        raise RenderError(f"Failed to render dataface: {e}") from 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(face, 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(face, 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/core/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.core.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

render_rows_layout(items: list[LayoutItem], executor: Executor, variables: VariableValues, available_width: float, available_height: float, gap: float, background: str | None = None, theme: str | None = None) -> LayoutResult

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]

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
LayoutResult

LayoutResult with SVG content and dimensions

Source code in dataface/core/render/layouts.py
def render_rows_layout(
    items: list[LayoutItem],
    executor: Executor,
    variables: VariableValues,
    available_width: float,
    available_height: float,
    gap: float,
    background: str | None = None,
    theme: str | None = None,
) -> LayoutResult:
    """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:
        LayoutResult with SVG content and dimensions
    """
    if not items:
        return LayoutResult("", available_width, available_height)

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

    for item in items:
        # Trust the normalizer - sizing.py sets all item dimensions
        item_height = item.height if item.height > 0 else available_height
        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:
            # Trust the normalizer - use pre-calculated height for positioning
            rendered_items.append(
                f'<g transform="translate(0, {current_y})">{item_svg}</g>'
            )
            current_y += item_height + gap

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

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

    content = f"{bg_rect}\n{''.join(rendered_items)}"
    return LayoutResult(content, available_width, total_height)

Columns Layout

render_cols_layout

render_cols_layout(items: list[LayoutItem], executor: Executor, variables: VariableValues, available_width: float, available_height: float, gap: float, background: str | None = None, theme: str | None = None) -> LayoutResult

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]

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
LayoutResult

LayoutResult with SVG content and dimensions

Source code in dataface/core/render/layouts.py
def render_cols_layout(
    items: list[LayoutItem],
    executor: Executor,
    variables: VariableValues,
    available_width: float,
    available_height: float,
    gap: float,
    background: str | None = None,
    theme: str | None = None,
) -> LayoutResult:
    """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:
        LayoutResult with SVG content and dimensions
    """
    if not items:
        return LayoutResult("", available_width, available_height)

    # 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 = _bg_rect(available_width, row_height, background)

    content = f"{bg_rect}\n{''.join(rendered_items)}"
    return LayoutResult(content, available_width, row_height)

Grid Layout

render_grid_layout

render_grid_layout(items: list[LayoutItem], executor: Executor, variables: VariableValues, available_width: float, available_height: float, columns: int, gap: float, background: str | None = None, theme: str | None = None) -> LayoutResult

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]

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
LayoutResult

LayoutResult with SVG content and dimensions

Source code in dataface/core/render/layouts.py
def render_grid_layout(
    items: list[LayoutItem],
    executor: Executor,
    variables: VariableValues,
    available_width: float,
    available_height: float,
    columns: int,
    gap: float,
    background: str | None = None,
    theme: str | None = None,
) -> LayoutResult:
    """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:
        LayoutResult with SVG content and dimensions
    """
    if not items:
        return LayoutResult("", available_width, available_height)

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

    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 = 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 = _bg_rect(available_width, available_height, background)

    content = f"{bg_rect}\n{''.join(rendered_items)}"
    return LayoutResult(content, available_width, available_height)

Tabs Layout

render_tabs_layout

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

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.

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: list[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
LayoutResult

LayoutResult with SVG content and dimensions

Source code in dataface/core/render/layouts.py
def render_tabs_layout(
    items: list[LayoutItem],
    executor: Executor,
    variables: VariableValues,
    available_width: float,
    available_height: float,
    tab_titles: list[str] | None = None,
    active_tab: int = 0,
    tab_position: str = "top",
    background: str | None = None,
    theme: str | None = None,
) -> LayoutResult:
    """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.

    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:
        LayoutResult with SVG content and dimensions
    """
    if not items:
        return LayoutResult("", available_width, available_height)

    # Get tab bar height from config
    from dataface.core.compile.config import get_config

    config = get_config()
    tab_bar_height = config.layout.tabs.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 = render_layout_item(
        active_item, executor, variables, available_width, content_height, theme
    )

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

    # Render tab bar directly using template
    # Note: Tab colors are hardcoded; should come from theme config
    tab_width = available_width / len(titles)
    tabs = [
        {
            "x": idx * tab_width,
            "title": title,
            "is_active": idx == active_tab,
            "fill_color": "#e0e0e0" if idx == active_tab else "#f5f5f5",
            "text_color": "#1a1a1a" if idx == active_tab else "#666666",
        }
        for idx, title in enumerate(titles)
    ]
    tab_bar_svg = render_template(
        "svg/tab_bar.svg",
        tabs=tabs,
        tab_width=tab_width,
        tab_bar_height=tab_bar_height,
    )

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

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

    content = f"{bg_rect}\n{tab_bar_svg}\n{content_svg}"
    return LayoutResult(content, available_width, available_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 | 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 specification from a chart definition and data.

PARAMETER DESCRIPTION
chart

CompiledChart definition (or legacy Chart type)

TYPE: CompiledChart | Any

data

List of dicts containing chart data

TYPE: list[dict[str, Any]]

width

Optional explicit width in pixels

TYPE: float | None DEFAULT: None

height

Optional explicit height in pixels

TYPE: float | None DEFAULT: None

theme

Optional Vega-Lite theme name to apply

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
dict[str, Any]

Vega-Lite specification dictionary

Source code in dataface/core/render/vega_lite.py
def generate_vega_lite_spec(
    chart: CompiledChart | 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 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
    """
    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 = chart.type
    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 = chart.x
    x_label = chart.x_label
    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 = chart.y
    y_label = chart.y_label
    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 = chart.color
    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 = chart.size
    if size_field:
        encoding["size"] = {"field": size_field, "type": "quantitative"}

    shape_field = chart.shape
    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
    if (
        chart_type == "bar"
        and chart.settings
        and chart.settings.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

    _apply_style(spec, chart.style)

    if chart.title:
        spec["title"] = {"text": chart.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: CompiledChart | 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) -> str

Render a chart to the specified format.

PARAMETER DESCRIPTION
chart

CompiledChart definition

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

height

Optional explicit height in pixels

TYPE: float | None DEFAULT: None

theme

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

TYPE: str | None DEFAULT: None

is_placeholder

If True, render with placeholder styling (reduced opacity, "add data" overlay). Used when chart has no query/data.

TYPE: bool DEFAULT: False

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/core/render/vega_lite.py
def render_chart(
    chart: CompiledChart | 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,
) -> 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')
        is_placeholder: If True, render with placeholder styling (reduced opacity,
                        "add data" overlay). Used when chart has no query/data.

    Returns:
        Rendered chart as string

    Raises:
        ValueError: If format is not supported
        ImportError: If vl-convert-python is not installed for non-JSON formats
    """
    # For JSON format, return normalized Dataface format for ALL chart types
    # This returns what users write in YAML (after normalization) + data
    if format == "json":
        import json

        dataface_json = _build_dataface_json(chart, data, width=width, height=height)
        return json.dumps(dataface_json, indent=2)

    # If placeholder mode and no data, generate placeholder data
    # (Only needed for SVG/PNG/PDF formats, not JSON)
    chart_type = chart.type
    render_data = data
    if is_placeholder and (not data or len(data) == 0):
        from dataface.core.render.placeholder import generate_placeholder_data

        render_data = generate_placeholder_data(chart_type, chart)

    # Handle special chart types that render directly to SVG (not via Vega-Lite)
    if chart_type in ("table", "spark_bar"):
        # Select the appropriate SVG renderer
        render_svg_func = (
            render_table_svg if chart_type == "table" else render_spark_bar_svg
        )

        if format == "svg":
            return render_svg_func(
                chart,
                render_data,
                width=width,
                height=height,
                theme=theme,
                is_placeholder=is_placeholder,
            )
        elif format in ["png", "pdf"]:
            # For PNG/PDF, render SVG first then convert using svglib + reportlab
            svg_content = render_svg_func(
                chart,
                render_data,
                width=width,
                height=height,
                theme=theme,
                is_placeholder=is_placeholder,
            )
            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} {chart_type} rendering. "
                    "Install with: pip install svglib reportlab"
                ) from None
        else:
            raise ValueError(
                f"Unsupported format: {format}. Supported: json, svg, png, pdf"
            )

    # Standard Vega-Lite chart rendering for SVG/PNG/PDF
    if format in ["svg", "png", "pdf"]:
        spec = generate_vega_lite_spec(
            chart, render_data, width=width, height=height, theme=theme
        )

        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"
            ) from None

        if format == "svg":
            # Note: Each chart currently renders individually. Batch rendering could
            # achieve ~30% speedup by combining charts into one vl-convert call.
            # See: plans/archive/VEGA_SVG_BATCH_CONVERSION_ANALYSIS.md
            svg_result = vlc.vegalite_to_svg(spec)

            # Apply placeholder styling if needed
            if is_placeholder:
                from dataface.core.render.placeholder import (
                    add_placeholder_overlay,
                    apply_placeholder_opacity,
                )

                # Get chart dimensions for overlay
                chart_width = width or 400
                chart_height = height or 300
                svg_result = apply_placeholder_opacity(svg_result)
                svg_result = add_placeholder_overlay(
                    svg_result, chart_width, chart_height
                )

            return svg_result
        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 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)

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 dataface.

RenderError

RenderError(message: str, element: str | None = 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/core/render/errors.py
def __init__(self, message: str, element: str | None = None):
    self.message = message
    self.element = element
    super().__init__(self._format_message())

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)