Compile effective chart presentation defaults before render
Problem
Move static chart presentation defaults out of renderer-time merge layering and into a compiled effective defaults contract so theme, structure, and config precedence become predictable.
Context
- Background research:
ai_notes/refactors/CHART_DEFAULTS_AND_PRECEDENCE_AUDIT.mdai_notes/refactors/COMPILED_PRESENTATION_DEFAULTS_ARCHITECTURE.md- The audit found that Dataface currently splits chart presentation defaults across:
- runtime config assembly in
dataface/core/compile/config.py - compile-time normalization in
dataface/core/compile/* - semantic enrichment in
dataface/core/render/chart/pipeline.pyanddecisions.py - renderer-time Vega-Lite config merging in
dataface/core/render/chart/standard_renderer.py - explicit spec-object generation in helpers such as
build_channel_encoding(),get_smart_x_axis_config(), andset_chart_title() - This creates a hidden precedence layer where generated explicit objects such as:
spec.titleencoding.x.axisencoding.y.axis- generated
scaleblocks can override structure/theme/config defaults that were correctly loaded upstream. - Concrete symptoms already observed:
- structure/theme config can set axis label or title behavior and still lose to generated explicit axis/title objects
settings.x_axisorsettings.y_axiscan feel more authoritative than config because they merge directly into explicit channel objects- single-metric and layered multi-metric charts do not currently apply the same defaults path
- Important boundary:
- We do still want a small amount of post-execute logic after data arrives.
- The better framing is:
- compile resolves references and static presentation defaults
- post-execute semantic resolution fills only authored semantic fields that were intentionally left
autoor omitted - mechanical render translates the fully-resolved chart plus compiled defaults into output
- Allowed post-execute resolution should be narrow and explicit:
chart_typewhen authoredtypeisauto- semantic field selection such as
x,y,color,metric, orthetaonly when authors intentionally left them unresolved andchart_enrichmentallows inference - semantic
formatonly when authored format is unresolved and format inference is enabled - semantic
zeroonly when authored zero behavior is unresolved and zero inference is enabled
- Allowed size- or data-dependent presentation logic should also be narrow and explicit:
- x-axis posture fields such as
labelAngle,labelAlign,labelBaseline - temporal
tickCount - measured label gutter or
labelPaddingadjustments for layouts like horizontal bars - dynamic
labelLimitexpansions driven by actual label content and width
- x-axis posture fields such as
- Static defaults such as theme, structure, title defaults, axis existence/orientation, legend placement, and base Vega config should not remain a renderer-owned merge problem.
- Not allowed as renderer-owned defaults after this refactor:
- late merging of theme/structure/base config to decide the effective static contract
- static axis title/label/grid/domain defaults
- static legend placement or title anchoring defaults
- hardcoded title styling or schema defaults that should come from compiled config
- Suggested object model:
- do not explode a giant resolved defaults blob into every chart
- instead compile one immutable presentation context per
CompiledFace - nested faces derive their context from the parent face context plus local overrides such as
theme,structure, and inherited style/default state - charts then read from the containing face context and apply only sparse chart-level authored overrides
- if implementation wants structural sharing instead of eagerly copying every subtree, that is acceptable, but the API exposed to render must still behave like one authoritative resolved contract
- Relevant code areas:
dataface/core/compile/config.pydataface/core/compile/compiler.pydataface/core/compile/normalizer.pydataface/core/compile/compiled_types.pydataface/core/render/chart/pipeline.pydataface/core/render/chart/presentation.pydataface/core/render/chart/spec_builders.pydataface/core/render/chart/standard_renderer.pydataface/core/render/chart/vega_lite_types.py- Constraint:
- Do not move data-dependent semantic enrichment into compile. The goal is to move only static presentation defaults out of the render-time merge stack.
Possible Solutions
- Patch precedence bugs one by one in render. Fastest short-term path, but it preserves the split-brain contract and keeps the renderer as the place where defaults are effectively decided.
- Recommended: add a compile sub-stage that materializes effective presentation defaults per face and make render consume that contract mechanically. This keeps compile as the place where static values become guaranteed, while still allowing a small late stage for data-dependent enrichment and sizing-aware presentation.
- Type current config and settings surfaces more strictly without changing where defaults are resolved. This would improve schema visibility, but it would mostly formalize today's confusing precedence rules instead of fixing them.
- Push everything, including semantic auto decisions, into compile. Rejected. Chart type auto-detection, unresolved field inference, and width/data-aware axis posture legitimately depend on executed data and final sizing context.
Plan
- Inventory the exact presentation inputs that should become part of the compiled defaults contract: - global runtime defaults - selected active structure - theme-paired structure overlay - selected theme - inherited face-level presentation state
- Classify each existing default path as one of: - static and should be compiled early - data-dependent and should stay post-execute - size-dependent and should stay near render/sizing - accidental duplication that should be removed
- Introduce a typed compiled presentation context object attached at the
CompiledFacelevel rather than duplicated into every chart. - Nested boards should derive their own context from the parent face context plus local overrides. - Prefer one easy-to-query authoritative object over a giant per-chart explosion of copied values. - Refactor standard chart rendering so: - top-level Vega config comes from compiled defaults - explicit axis/title/scale objects are seeded from compiled defaults - authored overrides still win - only the explicitly allowed semantic and size-dependent values are computed after data and sizing are known
- Unify single-series and layered-series chart paths so they derive from the same effective defaults contract.
- Add a broad precedence test matrix for the concrete failures this task is meant to eliminate: - structure/theme axis labels hidden but still rendered - structure/theme axis titles disabled but explicit generated titles still win - config-exposed values present in runtime config but not controlling emitted specs - parity between single-metric and layered-multi-metric behavior - authored override precedence over compiled face defaults - nested-face inheritance versus local override behavior - pass-through of each implementation-significant property through each allowed layer
- Update architectural docs or code comments where needed so future contributors understand the split between: - compiled static defaults - post-execute semantic resolution - dynamic presentation helpers
Implementation Progress
- Spawned from:
ai_notes/refactors/CHART_DEFAULTS_AND_PRECEDENCE_AUDIT.mdai_notes/refactors/COMPILED_PRESENTATION_DEFAULTS_ARCHITECTURE.md- Initial task framing decision:
- render should keep only the small set of data-dependent and size-dependent decisions
- static presentation defaults should move into a compiled effective-contract layer
Added
compile_effective_vega_config()topresentation.py: - Computes the fully-merged Vega config from
default_config.vega.config→ explicit structure → theme-paired structure → theme - Standalone function callable without a spec, enabling pre-compilation before render
- Moved
_to_plain_dict,_deep_merge, and_structure_vega_confighelpers fromstandard_renderer.pyintopresentation.pyto colocate with the compilation logic
Refactored apply_presentation_defaults() in standard_renderer.py:
- Now accepts optional effective_vega_config kwarg — when provided, skips recomputing the merged config
- Falls back to calling compile_effective_vega_config() when not provided (backward-compatible)
- Background handling (theme background key) stays in apply_presentation_defaults since it's spec-level, not config-level
- Removed _structure_vega_config from standard_renderer.py (now in presentation.py)
Tests added (5 new tests in test_standard_renderer.py):
- test_returns_base_defaults_with_no_theme_or_structure — base vega config returned
- test_structure_overrides_merge_onto_defaults — structure merges on top
- test_theme_overrides_merge_after_structure — theme wins over structure for shared keys
- test_default_theme_applies_when_theme_is_none — configured default_theme auto-applies
- test_apply_presentation_defaults_uses_compiled_config — pre-compiled config produces same result
QA Exploration
- [x] N/A — pure backend refactor, no UI changes
Review Feedback
- [ ] Review cleared