Dataface Tasks

Compile effective chart presentation defaults before render

IDDFT_CORE-COMPILE_EFFECTIVE_CHART_PRESENTATION_DEFAULTS_BEFORE_RENDER
Statuscompleted
Priorityp1
Milestonem1-ft-analytics-analyst-pilot
Ownersr-engineer-architect
Completed bydave
Completed2026-03-26

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.md
  • ai_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.py and decisions.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(), and set_chart_title()
  • This creates a hidden precedence layer where generated explicit objects such as:
  • spec.title
  • encoding.x.axis
  • encoding.y.axis
  • generated scale blocks 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_axis or settings.y_axis can 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 auto or omitted
    • mechanical render translates the fully-resolved chart plus compiled defaults into output
  • Allowed post-execute resolution should be narrow and explicit:
    • chart_type when authored type is auto
    • semantic field selection such as x, y, color, metric, or theta only when authors intentionally left them unresolved and chart_enrichment allows inference
    • semantic format only when authored format is unresolved and format inference is enabled
    • semantic zero only 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 labelPadding adjustments for layouts like horizontal bars
    • dynamic labelLimit expansions driven by actual label content and width
  • 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.py
  • dataface/core/compile/compiler.py
  • dataface/core/compile/normalizer.py
  • dataface/core/compile/compiled_types.py
  • dataface/core/render/chart/pipeline.py
  • dataface/core/render/chart/presentation.py
  • dataface/core/render/chart/spec_builders.py
  • dataface/core/render/chart/standard_renderer.py
  • dataface/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

  1. 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.
  2. 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.
  3. 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.
  4. 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

  1. 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
  2. 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
  3. Introduce a typed compiled presentation context object attached at the CompiledFace level 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.
  4. 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
  5. Unify single-series and layered-series chart paths so they derive from the same effective defaults contract.
  6. 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
  7. 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.md
  • ai_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() to presentation.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_config helpers from standard_renderer.py into presentation.py to 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