Chart Rendering Architecture¶
This page is the canonical contributor reference for how Dataface chart
tuning via style, compiled style resolution, and Vega-Lite emission are meant
to fit together.
Use it when you need to answer questions like:
- Where should a chart decision happen?
- What is the intended boundary between Dataface style contracts and Vega-Lite?
- Is a proposed cleanup, refactor, or new feature moving the system toward or away from the intended design?
For local chart philosophy and render-layer invariants, also read
dataface/core/render/chart/DESIGN.md in the repo.
For the named objects and their layer boundaries, also read Chart Object Model.
Purpose¶
Dataface owns a small, deliberate style system and compiles it down into Vega-Lite config where appropriate.
The architecture is:
- authored YAML expresses:
- chart semantics
- board or chart
style - compile validates and normalizes that input into typed Dataface contracts
- compile resolves style presets through
style.extendsand merges style patches into an authoritative compiled style object - render maps that compiled style to Vega-Lite config mechanically
- render maps chart semantics to a final Vega-Lite spec or another renderer family output
The key design goal is predictability:
- authored chart meaning stays stable through the pipeline
- style precedence is explicit and compiled before render
- Dataface does not pretend the whole app config is a Vega-Lite theme
- Vega-Lite is treated as an output target, not as the source of Dataface runtime style truth
Non-Goals¶
This architecture explicitly does not want:
- a second hidden data-transformation pipeline inside chart rendering
- renderer-side invention of new chart semantics after compile and enrichment
- many parallel names for the same Vega-Lite concept
- a giant runtime style model that is just a copy of the entire generated Vega-Lite schema
- overlapping style bags without crisp ownership
End-To-End Pipeline¶
The chart path is:
authored YAML
-> chart and style patch validation
-> style preset resolution + board style + chart style merging
-> compiled board-scoped style context
-> compiled chart contract
-> chart intent / semantic enrichment / resolution
-> style_to_vega_lite mapping
-> final Vega-Lite spec or direct SVG output
-> downstream render result
The stages have different responsibilities.
| Stage | Owns | Must not do |
|---|---|---|
| Authored YAML | User intent, explicit chart semantics, and style overlays |
Pretend all top-level config is the same kind of thing |
| Compile | Validation into patch/input models, normalization into compiled contracts, patch merging, typed compiled style objects, static defaults | Inspect live data or invent semantic chart meaning |
| Enrichment / resolution | Fill unresolved semantic fields from data and metadata | Override explicit authored meaning |
| Render | Mechanical translation to final output and Vega-Lite config emission | Re-decide style precedence or hide fallback policy in helpers |
Hard Ownership Rules¶
Query Owns Data Meaning¶
The query layer owns:
- grain
- grouping
- aggregation
- semantic ordering
- field meaning
The chart/render layer owns:
- visual encoding
- presentation
- reusable presentation defaults
If a chart "needs" regrouping, bucketing, re-aggregation, or semantic sorting, the default answer is to change the query or upstream model, not to add renderer-local data shaping.
Compile Owns Static Style And Defaults¶
Static presentation defaults should be decided before render and represented in Dataface contracts.
Examples:
- board style preset via
style.extends - board style overlay
- chart style overlay
- default mark styling
- default axis and title label angle / alignment
- chart sizing and spacing defaults
- chart inference defaults under
style.charts.inference - the compiled Vega-Lite config Dataface actually wants to emit
Render may consume these defaults, but it should not be the first place where they are merged, inferred, or re-decided.
The precedence is:
style preset + board style patch + chart style patch
-> authoritative compiled style object
-> style_to_vega_lite(...)
-> Vega-Lite config in the emitted spec
Enrichment Owns Unresolved Semantics¶
Only semantically unresolved fields should be filled from data-aware logic.
Examples:
type: auto- inferred
xory - inferred number/date formatting
- inferred scale behavior like
zero
Enrichment must not silently replace explicit authored values.
Render Owns Mechanical Emission¶
Once chart semantics are resolved, render should:
- choose the right renderer family
- map resolved chart state to Vega-Lite or SVG output
- map compiled Dataface style to renderer-specific config output
- attach top-level config and other validated passthrough
Render should not:
- invent new chart meaning
- hide style fallbacks in helper code
- duplicate defaults that already exist in compiled style contracts
Defaults Taxonomy¶
Dataface treats these as different layers:
Style Defaults¶
Examples:
- board spacing and padding defaults
- chart sizing defaults
- axis, legend, title, mark, and palette defaults
- typography defaults
- chart inference defaults nested under
style.charts.inference - non-Vega chart defaults like
style.charts.tableandstyle.charts.kpi - other presentation sections like
style.markdown
These compile into StyleCompiled.
Chart Semantic Defaults¶
Examples:
- unresolved
type: auto - inferred field roles
- inferred scale behavior
- semantic sort, stack, and encoding decisions
These belong with chart contracts and chart resolution.
Non-Style Engine Defaults¶
Examples:
- query behavior
- execution behavior
- non-presentation runtime policy
Keep this bucket small.
Vega-Lite Output Contract Rules¶
Dataface treats Vega-Lite as an output target, not as the full Dataface runtime style model.
That means:
- do not mirror the full upstream Vega-Lite schema as a giant runtime model
- do not keep generated Pydantic trees just because they exist
- do keep a deliberate, smaller typed output contract for the Vega-Lite config and spec surfaces Dataface actually emits
Three different bars matter:
Contract Exists¶
A Vega-Lite surface is represented in typed Dataface-owned output contracts.
Examples:
- a typed
VegaLiteConfigtarget model owned by Dataface - typed Dataface fields that validate against the output surfaces Dataface chooses to support
Contract Is Used¶
Normal authoring, compile, or render paths actually flow through those typed surfaces instead of bypassing them with ad hoc dicts or special-case assembly.
Examples:
style_to_vega_lite(...)produces a typed Vega-Lite config target- chart settings and emitted config flow through the owned contracts
- top-level passthrough validates before final spec emission if passthrough remains part of the supported surface
Contract Is Enforced¶
The test suite and render path make it difficult to bypass the contract without failing.
Examples:
- final emitted specs validate against the owned output contract
- tests prove
StyleCompiledreaches emitted Vega-Lite config on the main path - tests prove board and chart style precedence reaches the emitted spec
For future cleanup tasks, treat these as separate questions. A surface that "exists" but is not meaningfully used or enforced is still transitional.
Defaults And Precedence Rules¶
Defaults follow a simple model:
named style preset + board style patch + chart style patch
-> compiled style object
-> renderer-specific outputs
Rules:
- all static defaults live in YAML/config, not Python literals
- a style preset is a named collection of style defaults
- board and chart
styleare overlay patches, not separate systems - explicit chart-authored values override inherited style defaults
- enrichment fills only unresolved semantic fields
- direct Vega-Lite passthrough remains explicit, typed, and validated
- Vega-Lite built-in defaults remain allowed for fields Dataface does not own
If code is deciding a default with if missing: use 200 style logic, that is
usually an architecture violation unless the value is truly semantic and
data-dependent.
Style Model Rules¶
The style model is:
- shared nested style primitives (e.g.
TitleStyle,ScaleStyle,RangeStyle) and per-chart-type Compiled*StylePatch (e.g.CompiledBarStylePatch,CompiledAxisStylePatch) StyleCompiled- the full Dataface compiled style object
StylePatch- the recursive all-optional overlay model used for style presets and style patches
ChartPatch- the sparse authored chart input model
CompiledChart- the complete normalized chart contract
ResolvedChart- the later data-aware chart meaning object
VegaLiteConfig- the Vega-Lite-facing config model
style_to_vega_lite(style: StyleCompiled) -> VegaLiteConfig- the explicit mapping layer
Important constraints:
- shared nested submodels are good
- patch-to-compiled should exist on the chart side as well as the style side
- inheritance that tries to rename or delete upstream Vega fields is not
- mapping belongs in explicit functions, not shadow fields or serializer magic
- the shared style surface should be curated and owned by Dataface
- do not make the compiled style model equal to the full generated Vega-Lite schema
Chart-Type Style Rules¶
Vega-Lite organizes chart-style defaults mostly by mark config sections:
config.barconfig.lineconfig.pointconfig.areaconfig.arcconfig.text
Axis, legend, title, range, and view config are more global:
config.axisconfig.axisXconfig.axisYconfig.legendconfig.titleconfig.rangeconfig.view
Dataface follows that shape:
style.charts.axisstyle.charts.axis_xstyle.charts.axis_ystyle.charts.legendstyle.charts.titlestyle.charts.viewstyle.charts.rangestyle.charts.barstyle.charts.linestyle.charts.arcstyle.charts.pointstyle.charts.areastyle.charts.rectstyle.charts.labelstyle.charts.inference
Non-Vega chart renderers should live in the same chart-style namespace:
style.charts.tablestyle.charts.kpi
Then:
style_to_vega_lite(...)emits only the Vega-compatible subset- table and KPI renderers read their own chart-style sections directly
Seams And Boundaries¶
Any remaining compatibility shims should be treated as temporary.
Unacceptable long-term seams:
- dead generated model trees or schema generators that no longer match the owned architecture
- duplicated default logic in compile and render
- render-time theme or style merging that re-decides precedence
- tests that only prove a helper function works while the main pipeline still bypasses it
If a compatibility seam must remain, document:
- why it exists
- which path still depends on it
- what would let us delete it
Testing Requirements For Architecture Claims¶
When we claim an architecture cleanup is complete, tests should prove all three of these:
- The contract exists.
- The contract is used by the main code path.
- The contract is enforced against bypasses or regressions.
For the style architecture, that concretely means:
StylePatchmerges compile intoStyleCompiled- board and chart style precedence is tested
StyleCompiledmaps to emitted Vega-Lite config on the main path- non-Vega sections such as table or KPI styling do not leak into Vega-Lite config blindly
For chart architecture work, that usually means some mix of:
- compile-time typing and validation tests
- render-path tests that exercise normal authoring paths
- final spec validation against generated Vega-Lite contracts
- targeted integration tests that verify the authored -> contract -> spec path
- selective downstream behavioral checks when spec-level assertions are not enough
Architecture work is not complete if tests only cover helper functions while production chart paths still route around them.
CI Lanes For Chart And Style Tests¶
Chart and style tests are split into two lanes so that the fast gate stays fast and exploratory coverage still runs regularly.
Fast lane (every push / PR)¶
Runs via just test / just ci-test. Excludes tests marked @pytest.mark.slow.
Covers:
StylePatch→StyleCompiledmerge behavior and precedence- effective chart style view creation and fast-path reuse
style_to_vega_litemapping (axis, mark, orientation, top-level fields)- non-Vega sections (
table,kpi,inference) excluded fromVegaLiteConfig - closed contract validation (extra fields rejected)
- board-scoped
style_compiledmaterialization during compile - table and KPI renderer style consumption via
effective_style SvgRendererwiring ofeffective_styleto renderers
Slow lane (nightly schedule + manual dispatch)¶
Runs via just test-slow locally. In CI, the test-slow job triggers on
schedule (daily) and workflow_dispatch — it never blocks PR merges.
Covers:
- full chart property diagnostics sweep (
test_chart_property_diagnostics.py) - exploratory specimen corpora such as
chart-design-lab.yml(test_specimen_corpora.py) - integration benchmarks
Adding new tests¶
| Test type | Where | Marker |
|---|---|---|
| Style contract regression | tests/core/test_style_*.py |
(none — fast lane) |
| Effective style / pipeline | tests/core/test_effective_chart_style.py |
(none) |
| Renderer style consumption | tests/core/test_svg_renderer_style_consumption.py |
(none) |
| Property sweep / diagnostics | tests/scripts/test_chart_property_diagnostics.py |
@pytest.mark.slow |
| Specimen corpus regression | tests/integration/test_specimen_corpora.py |
@pytest.mark.slow |
What Cleanup Tasks Should Optimize For¶
The end state to maintain is:
- a small number of clear chart contracts
- compiled presentation defaults decided before render
- a canonical profile/mapping layer for Vega-Lite-native charts
- generated Vega-Lite models that meaningfully constrain real authoring and render paths
- minimal compatibility seams
- tests that assert the real pipeline rather than isolated helpers
When writing future cleanup tasks, prefer language like:
- "move this decision earlier into compile"
- "delete this render-time fallback"
- "route this path through the canonical profile seam"
- "replace ad hoc dict assembly with generated-contract validation"
- "add a test that proves the main authored path uses the new contract"
That framing keeps cleanup tied to architecture instead of style preference.