dataface 0.3.2.dev22__py3-none-any.whl → 0.3.2.dev111__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. dataface/__init__.py +70 -23
  2. dataface/agent_api/__init__.py +162 -68
  3. dataface/agent_api/_paths.py +9 -3
  4. dataface/agent_api/docs/yaml-reference.md +240 -106
  5. dataface/agent_api/init.py +1 -1
  6. dataface/agent_api/project_session.py +2 -1
  7. dataface/agent_api/serve.py +115 -0
  8. dataface/ai/agent.py +28 -2
  9. dataface/cli/_project.py +100 -1
  10. dataface/cli/commands/board_artifact.py +5 -2
  11. dataface/cli/commands/describe.py +7 -3
  12. dataface/cli/commands/docs.py +12 -3
  13. dataface/cli/commands/init.py +7 -14
  14. dataface/cli/commands/inspect.py +6 -1
  15. dataface/cli/commands/mcp_init.py +23 -43
  16. dataface/cli/commands/migrate.py +8 -3
  17. dataface/cli/commands/query.py +11 -8
  18. dataface/cli/commands/render.py +20 -9
  19. dataface/cli/commands/search.py +8 -2
  20. dataface/cli/commands/serve.py +26 -76
  21. dataface/cli/commands/skills_init.py +2 -19
  22. dataface/cli/commands/validate.py +7 -2
  23. dataface/cli/filesystem_project.py +5 -4
  24. dataface/cli/main.py +2 -19
  25. dataface/core/__init__.py +73 -28
  26. dataface/core/{compile/colors.py → colors.py} +20 -5
  27. dataface/core/compile/compiler.py +97 -34
  28. dataface/core/compile/data_table.py +7 -2
  29. dataface/core/compile/enrich.py +18 -44
  30. dataface/core/compile/face_warnings.py +46 -8
  31. dataface/core/compile/format.py +47 -1
  32. dataface/core/compile/inherit_graph.py +3 -3
  33. dataface/core/compile/inherit_registry.yaml +1 -1
  34. dataface/core/compile/migrations.py +112 -9
  35. dataface/core/compile/models/chart/normalized/kpi.py +0 -4
  36. dataface/core/compile/models/chart/resolved/_base.py +28 -10
  37. dataface/core/compile/models/chart/resolved/callout.py +2 -3
  38. dataface/core/compile/models/chart/resolved/kpi.py +7 -3
  39. dataface/core/compile/models/chart/resolved/table.py +16 -2
  40. dataface/core/compile/models/markers.py +1 -1
  41. dataface/core/compile/models/primitives.py +175 -2
  42. dataface/core/compile/models/source.py +5 -5
  43. dataface/core/compile/models/style/authored/__init__.py +32 -8
  44. dataface/core/compile/models/style/authored/_base.py +126 -69
  45. dataface/core/compile/models/style/authored/table.py +61 -3
  46. dataface/core/compile/models/style/resolved/__init__.py +12 -2
  47. dataface/core/compile/models/style/resolved/_base.py +142 -57
  48. dataface/core/compile/models/style/resolved/_cartesian.py +6 -11
  49. dataface/core/compile/models/style/resolved/_cascade.py +141 -54
  50. dataface/core/compile/models/style/resolved/area.py +1 -1
  51. dataface/core/compile/models/style/resolved/bar.py +1 -1
  52. dataface/core/compile/models/style/resolved/line.py +1 -1
  53. dataface/core/compile/models/style/resolved/scatter.py +3 -2
  54. dataface/core/compile/models/style/theme/__init__.py +36 -14
  55. dataface/core/compile/models/style/theme/_chart_base.py +17 -9
  56. dataface/core/compile/models/style/theme/axis.py +432 -312
  57. dataface/core/compile/models/style/theme/charts.py +10 -6
  58. dataface/core/compile/models/style/theme/kpi.py +2 -1
  59. dataface/core/compile/models/style/theme/page.py +5 -2
  60. dataface/core/compile/models/style/theme/table.py +2 -2
  61. dataface/core/compile/models/vega_lite/config.py +1 -1
  62. dataface/core/compile/normalize_charts.py +6 -2
  63. dataface/core/compile/normalize_queries.py +21 -7
  64. dataface/core/compile/resolve/__init__.py +2 -0
  65. dataface/core/compile/resolve/_axes.py +147 -44
  66. dataface/core/compile/resolve/_channels.py +1 -1
  67. dataface/core/compile/resolve/_dispatch.py +1 -1
  68. dataface/core/compile/resolve/_domain.py +49 -32
  69. dataface/core/compile/resolve/_kwargs.py +39 -12
  70. dataface/core/compile/resolve/_marks.py +8 -3
  71. dataface/core/compile/resolve/_palette.py +7 -3
  72. dataface/core/compile/resolve/_table.py +124 -33
  73. dataface/core/compile/resolve/area.py +48 -18
  74. dataface/core/compile/resolve/bar.py +245 -64
  75. dataface/core/compile/resolve/geo.py +10 -3
  76. dataface/core/compile/resolve/heatmap.py +16 -8
  77. dataface/core/compile/resolve/line.py +35 -12
  78. dataface/core/compile/resolve/pie.py +4 -1
  79. dataface/core/compile/resolve/pie_attachment.py +1 -1
  80. dataface/core/compile/resolve/scatter.py +25 -13
  81. dataface/core/compile/resolve/simple.py +25 -2
  82. dataface/core/compile/schema_renderers/prompt.py +1 -1
  83. dataface/core/compile/schema_renderers/textmate_grammar.py +8 -1
  84. dataface/core/compile/source_map.py +211 -28
  85. dataface/core/compile/style_cascade.py +126 -55
  86. dataface/core/compile/yaml_error_formatter.py +1 -1
  87. dataface/core/connections.py +10 -3
  88. dataface/core/dashboard.py +24 -18
  89. dataface/core/defaults/themes/_base.yaml +49 -39
  90. dataface/core/defaults/themes/cream.yaml +4 -4
  91. dataface/core/defaults/themes/diagnostics-title-baseline.yaml +1 -1
  92. dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +1 -1
  93. dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +1 -1
  94. dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +1 -1
  95. dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +1 -1
  96. dataface/core/defaults/themes/editorial.yaml +6 -6
  97. dataface/core/defaults/themes/neon.yaml +15 -8
  98. dataface/core/defaults/themes/plain.yaml +6 -4
  99. dataface/core/defaults/themes/stark.yaml +16 -7
  100. dataface/core/defaults/themes/vivid.yaml +15 -8
  101. dataface/core/diagnostics/__init__.py +2 -2
  102. dataface/core/diagnostics/codes_compile.py +13 -31
  103. dataface/core/diagnostics/codes_execute.py +79 -0
  104. dataface/core/diagnostics/codes_render.py +87 -28
  105. dataface/core/diagnostics/diagnostic.py +30 -0
  106. dataface/core/diagnostics/hints.py +19 -0
  107. dataface/core/dialects/base.py +17 -0
  108. dataface/core/dialects/postgres.py +14 -0
  109. dataface/core/dialects/redshift.py +5 -0
  110. dataface/core/execute/__init__.py +59 -14
  111. dataface/core/execute/adapters/dbt_adapter.py +4 -1
  112. dataface/core/execute/adapters/dbt_adapter_factory.py +19 -0
  113. dataface/core/execute/adapters/dbt_utils.py +84 -20
  114. dataface/core/execute/adapters/postgres_connection_manager.py +48 -0
  115. dataface/core/execute/adapters/sql_adapter.py +72 -4
  116. dataface/core/execute/dbt_jinja.py +65 -10
  117. dataface/core/execute/executor.py +69 -16
  118. dataface/core/font_measure.py +28 -0
  119. dataface/core/fonts.py +8 -5
  120. dataface/core/inspect/__init__.py +46 -6
  121. dataface/core/project.py +13 -1
  122. dataface/core/render/chart/data_table_attachment.py +14 -12
  123. dataface/core/render/chart/emitters/_cartesian.py +21 -18
  124. dataface/core/render/chart/emitters/_channels.py +56 -8
  125. dataface/core/render/chart/emitters/_label_overlap.py +18 -15
  126. dataface/core/render/chart/emitters/_measured_label_padding.py +4 -4
  127. dataface/core/render/chart/emitters/area.py +3 -3
  128. dataface/core/render/chart/emitters/bar.py +60 -35
  129. dataface/core/render/chart/emitters/geo.py +8 -13
  130. dataface/core/render/chart/emitters/heatmap.py +20 -26
  131. dataface/core/render/chart/emitters/line.py +2 -2
  132. dataface/core/render/chart/emitters/scatter.py +7 -30
  133. dataface/core/render/chart/features/baseline.py +22 -9
  134. dataface/core/render/chart/features/endpoint_labels.py +55 -32
  135. dataface/core/render/chart/features/mirror_axis.py +12 -9
  136. dataface/core/render/chart/features/value_labels.py +1 -1
  137. dataface/core/render/chart/kpi.py +11 -90
  138. dataface/core/render/chart/presentation.py +6 -8
  139. dataface/core/render/chart/rendering.py +3 -12
  140. dataface/core/render/chart/session.py +10 -0
  141. dataface/core/render/chart/spec.py +3 -0
  142. dataface/core/render/chart/spec_builders.py +18 -18
  143. dataface/core/render/chart/table.py +81 -36
  144. dataface/core/render/chart/table_support.py +2 -53
  145. dataface/core/render/chart/time_unit_detect.py +3 -2
  146. dataface/core/render/chart/translate.py +82 -44
  147. dataface/core/render/chart/type_inference.py +45 -25
  148. dataface/core/render/chart/validation.py +91 -0
  149. dataface/core/render/chart/vega_lite.py +30 -11
  150. dataface/core/render/chart/vl_field_maps.py +111 -76
  151. dataface/core/render/converters/chart.py +16 -6
  152. dataface/core/render/faces.py +3 -2
  153. dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.woff2 +0 -0
  154. dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.woff2 +0 -0
  155. dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -93
  156. dataface/core/render/fonts/_font_face.css +10 -0
  157. dataface/core/render/layout_sizing.py +267 -238
  158. dataface/core/render/numeral_expr.py +80 -0
  159. dataface/core/render/warnings/bar_band_width_too_narrow.py +64 -6
  160. dataface/core/render/warnings/layered_chart_shared_y_axis_scale_mismatch.py +25 -3
  161. dataface/core/render/warnings/likely_currency_or_percent_missing_formatter.py +12 -6
  162. dataface/core/render/warnings/palette_unsupported.py +6 -0
  163. dataface/core/render/warnings/pie_dominant_segment.py +1 -0
  164. dataface/core/render/warnings/pie_too_many_segments.py +1 -0
  165. dataface/core/render/warnings/point_map_out_of_projection.py +1 -0
  166. dataface/core/render/warnings/query_returned_zero_rows.py +1 -0
  167. dataface/core/render/warnings/redundant_encoding.py +20 -1
  168. dataface/core/render/warnings/registry.py +7 -3
  169. dataface/core/render/warnings/temporal_single_point.py +1 -0
  170. dataface/core/render/warnings/too_many_color_categories.py +1 -0
  171. dataface/core/render/warnings/too_many_x_categories.py +18 -2
  172. dataface/core/render/warnings/value_labels_crowd_width.py +9 -0
  173. dataface/core/render/warnings/y_encoding_mostly_null.py +1 -0
  174. dataface/core/render_format.py +18 -0
  175. dataface/core/text/format_d3.py +56 -0
  176. dataface/core/text/numeral_scale.py +218 -0
  177. dataface/core/utils.py +103 -10
  178. dataface/data/schemas/board-resolved/board-resolved.schema.json +9487 -7978
  179. dataface/data/schemas/yaml/0.3.2.json +1 -0
  180. dataface/data/schemas/yaml/manifest.json +7 -0
  181. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/METADATA +1 -1
  182. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/RECORD +185 -177
  183. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/WHEEL +0 -0
  184. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/entry_points.txt +0 -0
  185. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/licenses/LICENSE +0 -0
@@ -11,7 +11,7 @@ Public surface:
11
11
  resolve_mark() — merge global + family-level mark defaults
12
12
  insert_emoji_family() — insert emoji font into a CSS font-family stack
13
13
  apply_emoji_to_family() — conditionally insert emoji font by mode
14
- build_resolved_axis() — build a ResolvedAxisStyle from an AxisStyle
14
+ build_resolved_axis() — build a ResolvedAxisStyle from a BaseAxisStyle
15
15
  clear_resolve_style_cache() — evict the no-patch cache after config reloads
16
16
 
17
17
  Private helpers are prefixed with _ and not intended for callers outside
@@ -34,13 +34,18 @@ from dataface.core.compile.models.primitives import FontStyle, ResolvedFontStyle
34
34
  from dataface.core.compile.models.style.authored import PaginationConfig
35
35
  from dataface.core.compile.models.style.theme import (
36
36
  AxisLabelOverlapConfig,
37
- AxisStyle,
37
+ AxisXStyle,
38
+ AxisYStyle,
39
+ BaseScaleStyle,
38
40
  CalloutChartStyle,
39
41
  ChartsStyle,
42
+ DimensionLabelStyle,
43
+ DimensionTicksStyle,
40
44
  LegendStyle,
41
45
  SparkStyle,
42
46
  Style,
43
47
  TitleStyle,
48
+ XScaleStyle,
44
49
  font_weight_as_css,
45
50
  )
46
51
  from dataface.core.compile.vega_lite.mapping import effective_vega_config
@@ -49,16 +54,21 @@ from dataface.core.compile.vega_lite.mapping import effective_vega_config
49
54
  from dataface.core.fonts import NOTO_COLOR_EMOJI_FONT_FAMILY, NOTO_EMOJI_FONT_FAMILY
50
55
 
51
56
  from ._base import (
52
- ResolvedAxisDomainStyle,
53
57
  ResolvedAxisElementStyle,
54
58
  ResolvedAxisGridStyle,
55
59
  ResolvedAxisGridZeroStyle,
56
60
  ResolvedAxisLabelOverlapConfig,
61
+ ResolvedAxisLineStyle,
57
62
  ResolvedAxisStyle,
58
63
  ResolvedAxisTicksStyle,
59
64
  ResolvedChartsStyle,
60
65
  ResolvedLegendElementStyle,
61
66
  ResolvedLegendStyle,
67
+ ResolvedScaleContinuousStyle,
68
+ ResolvedScaleLogStyle,
69
+ ResolvedScalePowStyle,
70
+ ResolvedScaleStyle,
71
+ ResolvedScaleSymlogStyle,
62
72
  ResolvedStyle,
63
73
  )
64
74
  from .callout import ResolvedCalloutElementStyle, ResolvedCalloutStyle
@@ -440,30 +450,36 @@ def _seed_spark_colors(style: Style) -> Style:
440
450
  return style.model_copy(update={"charts": new_charts})
441
451
 
442
452
 
443
- # Fields on AxisElementStyle that are optional VL passthroughs:
444
- # inherited from parent axis to child (axis_x/y/etc.) via the InheritGraph,
445
- # then carried through build_resolved_axis to ResolvedAxisElementStyle.
453
+ # Fields on AxisLabelStyle (base) that are optional VL passthroughs.
446
454
  # "overlap" is handled explicitly (AxisLabelOverlapConfig → ResolvedAxisLabelOverlapConfig).
447
455
  # "align" is handled explicitly too (_resolve_own_side_align) — its authored
448
456
  # type carries inward/outward, which must be mapped against the axis's final
449
457
  # edge before it can land on the narrower resolved Literal["left","right"].
450
- _ELEM_PASSTHROUGH = (
458
+ _LABEL_PASSTHROUGH = (
451
459
  "max_width",
452
460
  "angle",
453
- "baseline",
454
- "separation",
461
+ "min_gap",
455
462
  "visible",
456
- "time_unit",
457
463
  "expr",
458
464
  "bound",
459
465
  "flush",
460
466
  "offset",
461
- "line_height",
462
- "anchor",
467
+ "format",
468
+ )
469
+
470
+ # Additional passthrough fields only on DimensionLabelStyle (AxisXStyle.labels).
471
+ # Absent from AxisLabelStyle (used by AxisYStyle.labels and axis_quantitative/band).
472
+ _ELEM_X_LABEL_PASSTHROUGH = (
473
+ "time_unit",
463
474
  "tilt_increments",
464
475
  "values",
465
476
  )
466
477
 
478
+ # AxisTitleStyle is deliberately thin (font/padding/angle/align/visible only)
479
+ # — a title is one short static string, not a per-tick label stream, so it
480
+ # has none of the label-overlap/VL-passthrough machinery above.
481
+ _TITLE_PASSTHROUGH = ("padding", "angle", "visible")
482
+
467
483
 
468
484
  def _resolve_own_side_align(
469
485
  align: Literal["left", "right", "center", "inward", "outward"] | None,
@@ -503,24 +519,77 @@ def _resolve_overlap_config(
503
519
  return None
504
520
  if overlap.tilt is None or overlap.skip is None:
505
521
  raise ValueError(
506
- f"axis.label.overlap has None fields after cascade: {overlap!r}; "
522
+ f"axis.labels.overlap has None fields after cascade: {overlap!r}; "
507
523
  "theme must supply both bools (tilt, skip) via _base.yaml"
508
524
  )
509
525
  return ResolvedAxisLabelOverlapConfig(tilt=overlap.tilt, skip=overlap.skip)
510
526
 
511
527
 
528
+ def build_resolved_scale(
529
+ scale: BaseScaleStyle | XScaleStyle | None,
530
+ ) -> ResolvedScaleStyle | None:
531
+ """Convert authored BaseScaleStyle/XScaleStyle to ResolvedScaleStyle.
532
+
533
+ Returns None when no scale was authored (axis or chart has no scale override).
534
+ continuous is None when not authored. x_reverse is populated only from
535
+ XScaleStyle; None on all other inputs.
536
+ """
537
+ if scale is None:
538
+ return None
539
+ return ResolvedScaleStyle(
540
+ round=scale.round,
541
+ clamp=scale.clamp,
542
+ nice=scale.nice,
543
+ padding=scale.padding,
544
+ headroom=scale.headroom,
545
+ values=scale.values,
546
+ continuous=(
547
+ ResolvedScaleContinuousStyle(
548
+ zero=scale.continuous.zero,
549
+ type=scale.continuous.type,
550
+ domain=scale.continuous.domain,
551
+ domain_strategy=scale.continuous.domain_strategy,
552
+ log=(
553
+ ResolvedScaleLogStyle(base=scale.continuous.log.base)
554
+ if scale.continuous.log is not None
555
+ else None
556
+ ),
557
+ pow=(
558
+ ResolvedScalePowStyle(exponent=scale.continuous.pow.exponent)
559
+ if scale.continuous.pow is not None
560
+ else None
561
+ ),
562
+ symlog=(
563
+ ResolvedScaleSymlogStyle(constant=scale.continuous.symlog.constant)
564
+ if scale.continuous.symlog is not None
565
+ else None
566
+ ),
567
+ )
568
+ if scale.continuous is not None
569
+ else None
570
+ ),
571
+ x_reverse=scale.x_reverse if isinstance(scale, XScaleStyle) else None,
572
+ )
573
+
574
+
512
575
  def build_resolved_axis(
513
- axis: AxisStyle,
576
+ axis: AxisXStyle | AxisYStyle,
514
577
  *,
578
+ band_position: float | None = None,
515
579
  edge: Literal["left", "right"] | None = None,
516
580
  tick_values: tuple[float, ...] = (),
517
581
  domain_max: float | EllipsisType = ...,
518
582
  domain_min: float | EllipsisType = ...,
519
583
  ) -> ResolvedAxisStyle:
520
- """Build the frozen, render-ready ResolvedAxisStyle from a merged AxisStyle.
584
+ """Build ResolvedAxisStyle from a channel-typed AxisXStyle or AxisYStyle.
585
+
586
+ Accepts AxisXStyle or AxisYStyle (after resolved_axis_style's merge chain).
587
+ band_position is passed explicitly since BandAxisStyle is structurally absent
588
+ from AxisXStyle/AxisYStyle — the caller extracts it from BandAxisStylePatch.
521
589
 
522
- ``edge`` is the axis's final left/right placement (None for a bottom/top
523
- axis). It resolves ``label.align``/
590
+ ``edge`` is the axis's final left/right placement, when known (None for a
591
+ bottom/top axis, or when the caller hasn't determined the edge yet — see
592
+ ``resolved_axis_style``'s docstring). It resolves ``labels.align``/
524
593
  ``title.align``'s inward/outward directives to concrete left/right (or
525
594
  None) via ``_resolve_own_side_align`` — the only place that mapping
526
595
  happens, so the render layer only ever sees left/right/None.
@@ -533,6 +602,26 @@ def build_resolved_axis(
533
602
  )
534
603
  return value
535
604
 
605
+ # grid.zero only exists on y-axis (AxisYStyle.grid is MeasureGridStyle, not BaseAxisGridStyle).
606
+ if isinstance(axis, AxisYStyle):
607
+ grid_zero: ResolvedAxisGridZeroStyle | None = (
608
+ ResolvedAxisGridZeroStyle(
609
+ color=_require(axis.grid.zero.color, "charts.axis_y.grid.zero.color"),
610
+ width=_require(axis.grid.zero.width, "charts.axis_y.grid.zero.width"),
611
+ )
612
+ if axis.grid.zero is not None
613
+ else None
614
+ )
615
+ else:
616
+ grid_zero = None
617
+
618
+ # label passthrough: x-axis adds tilt_increments/values/time_unit from DimensionLabelStyle.
619
+ label_kwargs = {f: getattr(axis.labels, f) for f in _LABEL_PASSTHROUGH}
620
+ if isinstance(axis.labels, DimensionLabelStyle):
621
+ label_kwargs.update(
622
+ {f: getattr(axis.labels, f) for f in _ELEM_X_LABEL_PASSTHROUGH}
623
+ )
624
+
536
625
  return ResolvedAxisStyle(
537
626
  grid=ResolvedAxisGridStyle(
538
627
  visible=_require(axis.grid.visible, "charts.axis.grid.visible"),
@@ -540,57 +629,52 @@ def build_resolved_axis(
540
629
  width=_require(axis.grid.width, "charts.axis.grid.width"),
541
630
  color=_require(axis.grid.color, "charts.axis.grid.color"),
542
631
  dash=axis.grid.dash,
543
- zero=ResolvedAxisGridZeroStyle(
544
- color=_require(
545
- axis.grid.zero.color if axis.grid.zero else None,
546
- "charts.axis.grid.zero.color",
547
- ),
548
- width=_require(
549
- axis.grid.zero.width if axis.grid.zero else None,
550
- "charts.axis.grid.zero.width",
551
- ),
552
- ),
632
+ zero=grid_zero,
553
633
  ),
554
- domain=ResolvedAxisDomainStyle(
555
- visible=_require(axis.domain.visible, "charts.axis.domain.visible"),
556
- width=_require(axis.domain.width, "charts.axis.domain.width"),
557
- color=_require(axis.domain.color, "charts.axis.domain.color"),
634
+ line=ResolvedAxisLineStyle(
635
+ visible=_require(axis.line.visible, "charts.axis.line.visible"),
636
+ width=_require(axis.line.width, "charts.axis.line.width"),
637
+ color=_require(axis.line.color, "charts.axis.line.color"),
558
638
  ),
559
639
  ticks=ResolvedAxisTicksStyle(
560
640
  visible=_require(axis.ticks.visible, "charts.axis.ticks.visible"),
561
641
  color=_require(axis.ticks.color, "charts.axis.ticks.color"),
562
642
  width=axis.ticks.width,
563
- size=axis.ticks.size,
643
+ length=axis.ticks.length,
644
+ offset=axis.ticks.offset,
564
645
  count=axis.ticks.count,
565
- interval=axis.ticks.interval,
566
646
  step=axis.ticks.step,
647
+ time_unit=(
648
+ axis.ticks.time_unit
649
+ if isinstance(axis.ticks, DimensionTicksStyle)
650
+ else None
651
+ ),
567
652
  ),
568
- label=ResolvedAxisElementStyle(
569
- font=resolve_cascaded_font(axis.label.font, "charts.axis.label.font"),
570
- padding=_require(axis.label.padding, "charts.axis.label.padding"),
571
- align=_resolve_own_side_align(axis.label.align, edge),
572
- overlap=_resolve_overlap_config(axis.label.overlap),
573
- **{f: getattr(axis.label, f) for f in _ELEM_PASSTHROUGH},
653
+ labels=ResolvedAxisElementStyle(
654
+ font=resolve_cascaded_font(axis.labels.font, "charts.axis.labels.font"),
655
+ padding=_require(axis.labels.padding, "charts.axis.labels.padding"),
656
+ align=_resolve_own_side_align(axis.labels.align, edge),
657
+ overlap=_resolve_overlap_config(axis.labels.overlap),
658
+ **label_kwargs,
574
659
  ),
575
660
  title=ResolvedAxisElementStyle(
576
661
  font=resolve_cascaded_font(axis.title.font, "charts.axis.title.font"),
577
- padding=_require(axis.title.padding, "charts.axis.title.padding"),
578
662
  align=_resolve_own_side_align(axis.title.align, edge),
579
- overlap=_resolve_overlap_config(axis.title.overlap),
580
- **{f: getattr(axis.title, f) for f in _ELEM_PASSTHROUGH},
663
+ **{f: getattr(axis.title, f) for f in _TITLE_PASSTHROUGH},
581
664
  ),
582
665
  position=axis.position,
583
- categorical_orient=axis.categorical_orient,
584
- mirror=axis.mirror,
585
- band_position=axis.band_position,
586
- offset=axis.offset,
587
- scale=axis.scale,
588
- fill=axis.fill,
589
- format=axis.format,
590
- values=axis.values,
591
- time_unit=axis.time_unit,
592
- type=axis.type,
593
- fiscal_year_start_month=axis.fiscal_year_start_month,
666
+ mirror=axis.mirror if isinstance(axis, AxisYStyle) else None,
667
+ band_position=band_position,
668
+ scale=build_resolved_scale(axis.scale),
669
+ fill=axis.fill if isinstance(axis, AxisXStyle) else None,
670
+ time_unit=axis.time_unit if isinstance(axis, AxisXStyle) else None,
671
+ type=axis.type if isinstance(axis, AxisXStyle) else None,
672
+ # fiscal_year_start_month is x-only (year/yearquarter bucket
673
+ # anchoring); non-x axes get the neutral calendar-convention default
674
+ # (1 = January) since the resolved field stays a required int.
675
+ fiscal_year_start_month=(
676
+ axis.fiscal_year_start_month if isinstance(axis, AxisXStyle) else 1
677
+ ),
594
678
  tick_values=tick_values,
595
679
  domain_max=None if domain_max is ... else domain_max,
596
680
  domain_min=None if domain_min is ... else domain_min,
@@ -731,7 +815,10 @@ def _build_resolved_charts(
731
815
  single_series_palette=resolved_single_series_palette,
732
816
  requested_alias_palette=_categorical_obj.requested_alias_palette,
733
817
  dashes=charts.dashes if charts.dashes is not None else [],
734
- axis=build_resolved_axis(charts.axis),
818
+ # axis is a raw BaseAxisStyle passthrough — no eager resolution here.
819
+ # resolved_axis_style() in style_cascade.py merges it with channel-typed
820
+ # axis_x/axis_y at emit time, seeding channel-specific fields from the typed value.
821
+ axis=charts.axis,
735
822
  axis_x=charts.axis_x,
736
823
  axis_y=charts.axis_y,
737
824
  axis_quantitative=charts.axis_quantitative,
@@ -14,7 +14,7 @@ class ResolvedAreaStyle(_SeriesCartesianResolvedStyle):
14
14
  """Area family style slice — self-contained, globals merged down.
15
15
 
16
16
  Every field is concrete after resolve; the area emitter reads only these.
17
- Inherits axis_x, axis_y, tooltip_format, background, series_label,
17
+ Inherits axis_x, axis_y, tooltip_format, series_label,
18
18
  endpoint_labels, single_series_fill, label_usable_ratio
19
19
  from the cartesian base hierarchy.
20
20
  """
@@ -16,7 +16,7 @@ class ResolvedBarStyle(_SeriesCartesianResolvedStyle):
16
16
  """Bar family style slice — self-contained, globals merged down.
17
17
 
18
18
  Every field is concrete after resolve; the bar emitter reads only these.
19
- Inherits axis_x, axis_y, tooltip_format, background, series_label,
19
+ Inherits axis_x, axis_y, tooltip_format, series_label,
20
20
  endpoint_labels, single_series_fill, label_usable_ratio
21
21
  from the cartesian base hierarchy.
22
22
  """
@@ -14,7 +14,7 @@ class ResolvedLineStyle(_SeriesCartesianResolvedStyle):
14
14
  """Line family style slice — self-contained, globals merged down.
15
15
 
16
16
  Every field is concrete after resolve; the line emitter reads only these.
17
- Inherits axis_x, axis_y, tooltip_format, background, series_label,
17
+ Inherits axis_x, axis_y, tooltip_format, series_label,
18
18
  endpoint_labels, single_series_fill, label_usable_ratio
19
19
  from the cartesian base hierarchy.
20
20
  """
@@ -4,7 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  from pydantic import ConfigDict, Field
6
6
 
7
- from dataface.core.compile.models.style.theme import PointMarkStyle, ScaleStyle
7
+ from dataface.core.compile.models.style.resolved._base import ResolvedScaleStyle
8
+ from dataface.core.compile.models.style.theme import PointMarkStyle
8
9
 
9
10
  from ._cartesian import _CartesianResolvedStyle
10
11
 
@@ -23,7 +24,7 @@ class ResolvedScatterStyle(_CartesianResolvedStyle):
23
24
  single_series_fill: str = Field(
24
25
  description="Fill color for single-series points (ink palette slot 0)."
25
26
  )
26
- scale: ScaleStyle | None = Field(
27
+ scale: ResolvedScaleStyle | None = Field(
27
28
  default=None,
28
29
  description="Board-level chart scale override; None = no override.",
29
30
  )
@@ -49,22 +49,33 @@ from dataface.core.compile.models.style.theme.area import (
49
49
  AreaLineStyle,
50
50
  )
51
51
  from dataface.core.compile.models.style.theme.axis import (
52
- AxisDomainStyle,
53
- AxisElementStyle,
54
- AxisGridStyle,
55
52
  AxisGridZeroStyle,
56
53
  AxisLabelOverlapConfig,
54
+ AxisLabelStyle,
55
+ AxisLineStyle,
57
56
  AxisMirrorStyle,
58
- AxisStyle,
59
- AxisTicksCadenceValidationMixin,
60
57
  AxisTicksStyle,
61
- ScaleBandStyle,
58
+ AxisTitleStyle,
59
+ AxisXStyle,
60
+ AxisYStyle,
61
+ BandAxisStyle,
62
+ BaseAxisGridStyle,
63
+ BaseAxisStyle,
64
+ BaseScaleStyle,
65
+ DimensionLabelStyle,
66
+ DimensionTicksStyle,
67
+ MeasureGridStyle,
68
+ QuantitativeAxisStyle,
69
+ ScaleContinuousStyle,
62
70
  ScaleDomainValidationMixin,
63
- ScaleStyle,
71
+ ScaleLogStyle,
72
+ ScalePowStyle,
73
+ ScaleSymlogStyle,
64
74
  TooltipBorderStyle,
65
75
  TooltipShadowStyle,
66
76
  TooltipSlotStyle,
67
77
  TooltipStyle,
78
+ XScaleStyle,
68
79
  )
69
80
  from dataface.core.compile.models.style.theme.bar import (
70
81
  BarChartMarksStyle,
@@ -243,15 +254,28 @@ __all__ = [
243
254
  "AreaLayerStyle",
244
255
  "AreaLineStyle",
245
256
  "AreaMarkStyle",
246
- "AxisDomainStyle",
247
- "AxisElementStyle",
248
- "AxisGridStyle",
249
257
  "AxisGridZeroStyle",
250
258
  "AxisLabelOverlapConfig",
259
+ "AxisLabelStyle",
260
+ "AxisLineStyle",
251
261
  "AxisMirrorStyle",
252
- "AxisStyle",
253
- "AxisTicksCadenceValidationMixin",
254
262
  "AxisTicksStyle",
263
+ "AxisTitleStyle",
264
+ "AxisXStyle",
265
+ "AxisYStyle",
266
+ "BandAxisStyle",
267
+ "BaseAxisGridStyle",
268
+ "BaseAxisStyle",
269
+ "BaseScaleStyle",
270
+ "DimensionLabelStyle",
271
+ "DimensionTicksStyle",
272
+ "MeasureGridStyle",
273
+ "QuantitativeAxisStyle",
274
+ "ScaleContinuousStyle",
275
+ "ScaleLogStyle",
276
+ "ScalePowStyle",
277
+ "ScaleSymlogStyle",
278
+ "XScaleStyle",
255
279
  "BarChartMarksStyle",
256
280
  "BarChartStyle",
257
281
  "BarLayerStyle",
@@ -321,9 +345,7 @@ __all__ = [
321
345
  "RectMarkStyle",
322
346
  "RootFontStyle",
323
347
  "RuleMarkStyle",
324
- "ScaleBandStyle",
325
348
  "ScaleDomainValidationMixin",
326
- "ScaleStyle",
327
349
  "ScatterChartMarksStyle",
328
350
  "ScatterChartStyle",
329
351
  "ScatterLayerStyle",
@@ -10,8 +10,12 @@ if TYPE_CHECKING:
10
10
  # finishes executing). The import here is TYPE_CHECKING-only to give mypy and
11
11
  # ruff the static definitions they need without creating a circular import.
12
12
  from dataface.core.compile.models.style.authored import ( # noqa: PLC0415
13
- AxisStylePatch,
13
+ AxisXStylePatch,
14
+ AxisYStylePatch,
15
+ BandAxisStylePatch,
16
+ BaseAxisStylePatch,
14
17
  DataTableStylePatch,
18
+ QuantitativeAxisStylePatch,
15
19
  TooltipStylePatch,
16
20
  )
17
21
 
@@ -32,7 +36,7 @@ from dataface.core.compile.models.primitives import (
32
36
  StaticGradientColorStyle,
33
37
  )
34
38
  from dataface.core.compile.models.style.theme.axis import (
35
- ScaleStyle,
39
+ BaseScaleStyle,
36
40
  TooltipStyle,
37
41
  )
38
42
  from dataface.core.compile.models.style.theme.board import (
@@ -148,28 +152,30 @@ class _CartesianChartStyle(_ChartStyleBaseAllOptional):
148
152
  # inherits the global axis at render time via resolved_axis_style's merge over
149
153
  # the fully-resolved base axis. Without this, apply_inherit slot-fills these
150
154
  # patches from the generic charts.font (prose ink) — so merely authoring an
151
- # unrelated field like bar.axis_x.label.padding materializes label.font and
155
+ # unrelated field like bar.axis_x.labels.padding materializes labels.font and
152
156
  # drags prose ink into the label color, overriding the theme's muted axis
153
157
  # color at the merge. Mirrors the `title` sentinel above (chart-local title
154
158
  # is likewise a skip-slots sparse patch). The axis element font/color/family
155
159
  # cascade is owned by the global axis nodes, not the per-family override.
156
- axis: Annotated[AxisStylePatch | None, SkipInheritSlots()] = Field(
160
+ axis: Annotated[BaseAxisStylePatch | None, SkipInheritSlots()] = Field(
157
161
  default=None,
158
162
  description="Override applied to both x and y axes; None inherits the global axis at render.",
159
163
  )
160
- axis_x: Annotated[AxisStylePatch | None, SkipInheritSlots()] = Field(
164
+ axis_x: Annotated[AxisXStylePatch | None, SkipInheritSlots()] = Field(
161
165
  default=None,
162
166
  description="Per-chart-type x-axis style overrides; None inherits the global axis_x at render.",
163
167
  )
164
- axis_y: Annotated[AxisStylePatch | None, SkipInheritSlots()] = Field(
168
+ axis_y: Annotated[AxisYStylePatch | None, SkipInheritSlots()] = Field(
165
169
  default=None,
166
170
  description="Per-chart-type y-axis style overrides; None inherits the global axis_y at render.",
167
171
  )
168
- axis_quantitative: Annotated[AxisStylePatch | None, SkipInheritSlots()] = Field(
172
+ axis_quantitative: Annotated[
173
+ QuantitativeAxisStylePatch | None, SkipInheritSlots()
174
+ ] = Field(
169
175
  default=None,
170
176
  description="Per-chart-type quantitative-axis overrides; None inherits the global axis_quantitative at render.",
171
177
  )
172
- axis_band: Annotated[AxisStylePatch | None, SkipInheritSlots()] = Field(
178
+ axis_band: Annotated[BandAxisStylePatch | None, SkipInheritSlots()] = Field(
173
179
  default=None,
174
180
  description="Per-chart-type categorical (band) axis overrides; None inherits the global band axis at render.",
175
181
  )
@@ -181,7 +187,9 @@ class _CartesianChartStyle(_ChartStyleBaseAllOptional):
181
187
  default=None,
182
188
  description="Default time format for temporal axes (D3 time format string); None inherits from theme.",
183
189
  )
184
- scale: ScaleStyle | None = Field(
190
+ # BaseScaleStyle (not XScaleStyle): this slot merges onto both x and y axes,
191
+ # so it must not carry x_reverse (which is structurally absent on BaseScaleStyle).
192
+ scale: BaseScaleStyle | None = Field(
185
193
  default=None,
186
194
  description="Chart-type encoding scale overrides applied to both x and y; None means no override.",
187
195
  )