dataface 0.3.2.dev22__py3-none-any.whl → 0.3.2.dev150__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 (227) hide show
  1. dataface/DATAFACE_SYNTAX.md +15 -4
  2. dataface/__init__.py +70 -23
  3. dataface/agent_api/__init__.py +162 -68
  4. dataface/agent_api/_paths.py +9 -3
  5. dataface/agent_api/docs/yaml-reference.md +273 -239
  6. dataface/agent_api/init.py +1 -1
  7. dataface/agent_api/project_session.py +2 -1
  8. dataface/agent_api/serve.py +113 -0
  9. dataface/agent_api/validate.py +8 -0
  10. dataface/ai/agent.py +28 -2
  11. dataface/cli/_project.py +100 -1
  12. dataface/cli/commands/board_artifact.py +5 -2
  13. dataface/cli/commands/describe.py +7 -3
  14. dataface/cli/commands/docs.py +12 -3
  15. dataface/cli/commands/init.py +7 -14
  16. dataface/cli/commands/inspect.py +6 -1
  17. dataface/cli/commands/mcp_init.py +23 -43
  18. dataface/cli/commands/migrate.py +8 -3
  19. dataface/cli/commands/query.py +11 -8
  20. dataface/cli/commands/render.py +26 -9
  21. dataface/cli/commands/search.py +8 -2
  22. dataface/cli/commands/serve.py +25 -81
  23. dataface/cli/commands/skills_init.py +2 -19
  24. dataface/cli/commands/validate.py +7 -2
  25. dataface/cli/filesystem_project.py +5 -4
  26. dataface/cli/main.py +22 -33
  27. dataface/core/__init__.py +73 -28
  28. dataface/core/aliases.py +27 -0
  29. dataface/core/{compile/colors.py → colors.py} +20 -5
  30. dataface/core/compile/__init__.py +0 -8
  31. dataface/core/compile/channel.py +0 -13
  32. dataface/core/compile/compiler.py +161 -96
  33. dataface/core/compile/data_table.py +7 -2
  34. dataface/core/compile/enrich.py +18 -44
  35. dataface/core/compile/face_warnings.py +46 -8
  36. dataface/core/compile/format.py +47 -1
  37. dataface/core/compile/inherit_graph.py +3 -3
  38. dataface/core/compile/inherit_registry.yaml +1 -1
  39. dataface/core/compile/introspection.py +2 -13
  40. dataface/core/compile/migrations.py +112 -9
  41. dataface/core/compile/models/chart/authored/_base.py +1 -1
  42. dataface/core/compile/models/chart/normalized/__init__.py +2 -0
  43. dataface/core/compile/models/chart/normalized/_base.py +26 -0
  44. dataface/core/compile/models/chart/normalized/kpi.py +0 -4
  45. dataface/core/compile/models/chart/resolved/_base.py +28 -10
  46. dataface/core/compile/models/chart/resolved/callout.py +2 -3
  47. dataface/core/compile/models/chart/resolved/kpi.py +7 -3
  48. dataface/core/compile/models/chart/resolved/table.py +16 -2
  49. dataface/core/compile/models/config.py +5 -0
  50. dataface/core/compile/models/face/authored.py +24 -89
  51. dataface/core/compile/models/markers.py +1 -1
  52. dataface/core/compile/models/primitives.py +175 -2
  53. dataface/core/compile/models/query/authored.py +1 -119
  54. dataface/core/compile/models/refs.py +0 -2
  55. dataface/core/compile/models/source.py +78 -164
  56. dataface/core/compile/models/style/authored/__init__.py +32 -8
  57. dataface/core/compile/models/style/authored/_base.py +126 -69
  58. dataface/core/compile/models/style/authored/table.py +61 -3
  59. dataface/core/compile/models/style/resolved/__init__.py +12 -2
  60. dataface/core/compile/models/style/resolved/_base.py +142 -57
  61. dataface/core/compile/models/style/resolved/_cartesian.py +6 -11
  62. dataface/core/compile/models/style/resolved/_cascade.py +141 -54
  63. dataface/core/compile/models/style/resolved/area.py +1 -1
  64. dataface/core/compile/models/style/resolved/bar.py +1 -1
  65. dataface/core/compile/models/style/resolved/line.py +1 -1
  66. dataface/core/compile/models/style/resolved/scatter.py +3 -2
  67. dataface/core/compile/models/style/theme/__init__.py +36 -14
  68. dataface/core/compile/models/style/theme/_chart_base.py +17 -9
  69. dataface/core/compile/models/style/theme/axis.py +432 -312
  70. dataface/core/compile/models/style/theme/charts.py +10 -6
  71. dataface/core/compile/models/style/theme/kpi.py +2 -1
  72. dataface/core/compile/models/style/theme/page.py +5 -2
  73. dataface/core/compile/models/style/theme/table.py +2 -2
  74. dataface/core/compile/models/style/theme/variables.py +0 -3
  75. dataface/core/compile/models/vega_lite/config.py +1 -1
  76. dataface/core/compile/normalize_charts.py +10 -154
  77. dataface/core/compile/normalize_layout.py +20 -284
  78. dataface/core/compile/normalize_queries.py +24 -513
  79. dataface/core/compile/normalizer.py +4 -10
  80. dataface/core/compile/palette.py +19 -7
  81. dataface/core/compile/resolve/__init__.py +2 -0
  82. dataface/core/compile/resolve/_axes.py +147 -44
  83. dataface/core/compile/resolve/_channels.py +1 -1
  84. dataface/core/compile/resolve/_dispatch.py +1 -1
  85. dataface/core/compile/resolve/_domain.py +49 -32
  86. dataface/core/compile/resolve/_kwargs.py +39 -12
  87. dataface/core/compile/resolve/_marks.py +8 -3
  88. dataface/core/compile/resolve/_palette.py +7 -3
  89. dataface/core/compile/resolve/_table.py +124 -33
  90. dataface/core/compile/resolve/area.py +48 -18
  91. dataface/core/compile/resolve/bar.py +245 -64
  92. dataface/core/compile/resolve/geo.py +10 -3
  93. dataface/core/compile/resolve/heatmap.py +16 -8
  94. dataface/core/compile/resolve/line.py +35 -12
  95. dataface/core/compile/resolve/pie.py +4 -1
  96. dataface/core/compile/resolve/pie_attachment.py +1 -1
  97. dataface/core/compile/resolve/scatter.py +25 -13
  98. dataface/core/compile/resolve/simple.py +25 -2
  99. dataface/core/compile/schema_renderers/prompt.py +1 -1
  100. dataface/core/compile/schema_renderers/textmate_grammar.py +8 -1
  101. dataface/core/compile/source_map.py +211 -28
  102. dataface/core/compile/style_cascade.py +126 -55
  103. dataface/core/compile/validator.py +121 -44
  104. dataface/core/compile/yaml_error_formatter.py +108 -3
  105. dataface/core/connections.py +10 -3
  106. dataface/core/dashboard.py +27 -19
  107. dataface/core/defaults/default_config.yml +4 -0
  108. dataface/core/defaults/themes/_base.yaml +49 -40
  109. dataface/core/defaults/themes/cream.yaml +4 -4
  110. dataface/core/defaults/themes/diagnostics-title-baseline.yaml +1 -1
  111. dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +1 -1
  112. dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +1 -1
  113. dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +1 -1
  114. dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +1 -1
  115. dataface/core/defaults/themes/editorial.yaml +9 -6
  116. dataface/core/defaults/themes/neon.yaml +15 -8
  117. dataface/core/defaults/themes/plain.yaml +6 -4
  118. dataface/core/defaults/themes/stark.yaml +24 -7
  119. dataface/core/defaults/themes/vivid.yaml +15 -8
  120. dataface/core/diagnostics/__init__.py +12 -2
  121. dataface/core/diagnostics/codes_compile.py +17 -33
  122. dataface/core/diagnostics/codes_execute.py +149 -0
  123. dataface/core/diagnostics/codes_render.py +144 -31
  124. dataface/core/diagnostics/diagnostic.py +30 -0
  125. dataface/core/diagnostics/hints.py +19 -0
  126. dataface/core/dialects/base.py +17 -0
  127. dataface/core/dialects/postgres.py +14 -0
  128. dataface/core/dialects/redshift.py +5 -0
  129. dataface/core/execute/__init__.py +59 -14
  130. dataface/core/execute/adapters/__init__.py +1 -1
  131. dataface/core/execute/adapters/dbt_adapter.py +4 -1
  132. dataface/core/execute/adapters/dbt_adapter_factory.py +19 -0
  133. dataface/core/execute/adapters/dbt_utils.py +84 -20
  134. dataface/core/execute/adapters/postgres_connection_manager.py +48 -0
  135. dataface/core/execute/adapters/sql_adapter.py +72 -4
  136. dataface/core/execute/cache_keys.py +4 -9
  137. dataface/core/execute/dbt_jinja.py +65 -10
  138. dataface/core/execute/executor.py +94 -19
  139. dataface/core/execute/file_source_materializer.py +185 -32
  140. dataface/core/execute/trivial_local_cache.py +5 -2
  141. dataface/core/font_measure.py +28 -0
  142. dataface/core/fonts.py +8 -5
  143. dataface/core/inspect/__init__.py +46 -7
  144. dataface/core/project.py +19 -3
  145. dataface/core/render/chart/data_table_attachment.py +14 -12
  146. dataface/core/render/chart/emitters/_cartesian.py +21 -18
  147. dataface/core/render/chart/emitters/_channels.py +56 -8
  148. dataface/core/render/chart/emitters/_label_overlap.py +18 -15
  149. dataface/core/render/chart/emitters/_measured_label_padding.py +4 -4
  150. dataface/core/render/chart/emitters/area.py +3 -3
  151. dataface/core/render/chart/emitters/bar.py +60 -35
  152. dataface/core/render/chart/emitters/geo.py +8 -13
  153. dataface/core/render/chart/emitters/heatmap.py +20 -26
  154. dataface/core/render/chart/emitters/line.py +2 -2
  155. dataface/core/render/chart/emitters/scatter.py +7 -30
  156. dataface/core/render/chart/features/baseline.py +22 -9
  157. dataface/core/render/chart/features/endpoint_labels.py +55 -32
  158. dataface/core/render/chart/features/mirror_axis.py +12 -9
  159. dataface/core/render/chart/features/value_labels.py +1 -1
  160. dataface/core/render/chart/kpi.py +11 -90
  161. dataface/core/render/chart/mark_extents.py +339 -0
  162. dataface/core/render/chart/presentation.py +6 -8
  163. dataface/core/render/chart/rendering.py +100 -46
  164. dataface/core/render/chart/session.py +10 -0
  165. dataface/core/render/chart/spec.py +3 -0
  166. dataface/core/render/chart/spec_builders.py +18 -18
  167. dataface/core/render/chart/table.py +81 -36
  168. dataface/core/render/chart/table_support.py +2 -53
  169. dataface/core/render/chart/time_unit_detect.py +3 -2
  170. dataface/core/render/chart/translate.py +82 -44
  171. dataface/core/render/chart/type_inference.py +45 -25
  172. dataface/core/render/chart/validation.py +91 -0
  173. dataface/core/render/chart/vega_lite.py +30 -11
  174. dataface/core/render/chart/vl_field_maps.py +111 -76
  175. dataface/core/render/conditions.py +122 -0
  176. dataface/core/render/controls.py +60 -187
  177. dataface/core/render/converters/chart.py +16 -6
  178. dataface/core/render/data_format.py +1 -1
  179. dataface/core/render/face_resolve.py +7 -0
  180. dataface/core/render/faces.py +97 -32
  181. dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.woff2 +0 -0
  182. dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.woff2 +0 -0
  183. dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -93
  184. dataface/core/render/fonts/_font_face.css +10 -0
  185. dataface/core/render/layout_sizing.py +313 -238
  186. dataface/core/render/numeral_expr.py +80 -0
  187. dataface/core/render/renderer.py +14 -1
  188. dataface/core/render/sizing.py +42 -105
  189. dataface/core/render/svg_utils.py +60 -14
  190. dataface/core/render/terminal.py +1 -1
  191. dataface/core/render/variables_layout.py +211 -0
  192. dataface/core/render/variables_resolve.py +376 -0
  193. dataface/core/render/variables_strip.py +291 -368
  194. dataface/core/render/warnings/bar_band_width_too_narrow.py +64 -6
  195. dataface/core/render/warnings/base.py +18 -0
  196. dataface/core/render/warnings/layered_chart_shared_y_axis_scale_mismatch.py +25 -3
  197. dataface/core/render/warnings/layout_min_exceeds_height.py +68 -0
  198. dataface/core/render/warnings/likely_currency_or_percent_missing_formatter.py +12 -6
  199. dataface/core/render/warnings/palette_unsupported.py +6 -0
  200. dataface/core/render/warnings/pie_dominant_segment.py +1 -0
  201. dataface/core/render/warnings/pie_too_many_segments.py +1 -0
  202. dataface/core/render/warnings/point_map_out_of_projection.py +1 -0
  203. dataface/core/render/warnings/query_returned_zero_rows.py +1 -0
  204. dataface/core/render/warnings/redundant_encoding.py +20 -1
  205. dataface/core/render/warnings/registry.py +9 -3
  206. dataface/core/render/warnings/temporal_single_point.py +1 -0
  207. dataface/core/render/warnings/too_many_color_categories.py +1 -0
  208. dataface/core/render/warnings/too_many_x_categories.py +18 -2
  209. dataface/core/render/warnings/value_labels_crowd_width.py +9 -0
  210. dataface/core/render/warnings/y_encoding_mostly_null.py +1 -0
  211. dataface/core/render_format.py +18 -0
  212. dataface/core/serve/alias_index.py +16 -22
  213. dataface/core/text/format_d3.py +56 -0
  214. dataface/core/text/numeral_scale.py +218 -0
  215. dataface/core/utils.py +103 -10
  216. dataface/data/schemas/board-resolved/board-resolved.schema.json +8589 -7578
  217. dataface/data/schemas/yaml/0.3.2.json +1 -0
  218. dataface/data/schemas/yaml/manifest.json +7 -0
  219. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/METADATA +1 -3
  220. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/RECORD +223 -213
  221. dataface/core/inspect/partials/categorical.yml +0 -38
  222. dataface/core/inspect/partials/date.yml +0 -38
  223. dataface/core/inspect/partials/numeric.yml +0 -53
  224. dataface/core/inspect/templates/charts.yml +0 -94
  225. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/WHEEL +0 -0
  226. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/entry_points.txt +0 -0
  227. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/licenses/LICENSE +0 -0
@@ -2,10 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Literal
5
+ from typing import Any, Literal
6
6
 
7
7
  from pydantic import BaseModel
8
8
 
9
+ from dataface.core.compile.models.style.resolved._base import ResolvedScaleStyle
9
10
  from dataface.core.compile.models.style.theme.marks import BarMarkStyle
10
11
  from dataface.core.compile.tick_values import numeric_domain_bounds
11
12
  from dataface.core.render.chart._types import VLDict
@@ -19,40 +20,76 @@ from dataface.core.render.chart.emitters._measured_label_padding import (
19
20
  quantitative_tick_labels,
20
21
  )
21
22
 
22
- SCALE_FIELD_MAP: dict[str, str] = {
23
- # ``paddingInner`` / ``paddingOuter`` are the encoding-level VL field names
24
- # for band-scale gutter and edge padding. The ``bandPaddingInner`` /
25
- # ``bandPaddingOuter`` variants only work at the global config level
26
- # (``config.scale.bandPaddingInner``) — they are silently ignored when
27
- # placed on ``encoding.{x,y}.scale``.
28
- "band_padding_inner": "paddingInner",
29
- "band_padding_outer": "paddingOuter",
30
- "point_padding": "pointPadding",
31
- "rect_band_padding_inner": "rectBandPaddingInner",
32
- "round": "round",
23
+ # ``type``/log.base/pow.exponent/symlog.constant emit log/pow/symlog scale
24
+ # config. Must not be omitted — a missed field silently resolves to None and
25
+ # drops scale config from the emitted spec.
26
+ _SCALE_CONTINUOUS_FIELD_MAP: dict[str, str] = {
33
27
  "zero": "zero",
34
- "clamp": "clamp",
35
- "continuous_padding": "continuousPadding",
36
- "quantile_count": "quantileCount",
37
- "quantize_count": "quantizeCount",
38
- "use_unaggregated_domain": "useUnaggregatedDomain",
39
- "x_reverse": "xReverse",
40
28
  "type": "type",
41
- "base": "base",
42
- "exponent": "exponent",
43
- "constant": "constant",
29
+ # domain and nice are encoding-level only (see SCALE_ENCODING_FIELD_MAP).
44
30
  }
45
-
46
- # Encoding-level-only scale fields applied to encoding.x.scale / encoding.y.scale.
31
+ # Universal top-level scale fields (not in any sub-group).
32
+ _SCALE_UNIVERSAL_FIELD_MAP: dict[str, str] = {
33
+ "round": "round",
34
+ "clamp": "clamp",
35
+ }
36
+ # Encoding-level-only scale fields: nice, padding shorthand. domain is read
37
+ # explicitly from scale.continuous.domain below (nested, not a flat field).
38
+ # Applied to encoding.x.scale / encoding.y.scale only (not VL config).
47
39
  SCALE_ENCODING_FIELD_MAP: dict[str, str] = {
48
- "domain": "domain",
49
40
  "nice": "nice",
50
41
  # Unified shorthand. VL dispatches based on scale type (bandPaddingOuter,
51
42
  # pointPadding, continuousPadding). Encoding-level only.
52
43
  "padding": "padding",
53
44
  }
45
+ # x-channel-only scale fields.
46
+ _SCALE_X_ONLY_FIELD_MAP: dict[str, str] = {
47
+ "x_reverse": "xReverse",
48
+ }
49
+
54
50
 
55
- from typing import Any
51
+ def emit_resolved_scale_vl(
52
+ scale: ResolvedScaleStyle | None,
53
+ *,
54
+ include_encoding_fields: bool = True,
55
+ include_x_only: bool = False,
56
+ ) -> dict[str, Any]:
57
+ """Emit a VL scale dict from a ResolvedScaleStyle (non-None fields only).
58
+
59
+ Args:
60
+ scale: Resolved scale to emit. Returns {} when None.
61
+ include_encoding_fields: When True (default), includes domain/nice/padding
62
+ (encoding-level-only fields). Set False for VL config-level emit.
63
+ include_x_only: When True, includes xReverse (x-channel only).
64
+ """
65
+ if scale is None:
66
+ return {}
67
+ out: dict[str, Any] = {}
68
+ out.update(map_fields(scale, _SCALE_UNIVERSAL_FIELD_MAP))
69
+ if scale.continuous is not None:
70
+ out.update(map_fields(scale.continuous, _SCALE_CONTINUOUS_FIELD_MAP))
71
+ if scale.continuous.log is not None and scale.continuous.log.base is not None:
72
+ out["base"] = scale.continuous.log.base
73
+ if (
74
+ scale.continuous.pow is not None
75
+ and scale.continuous.pow.exponent is not None
76
+ ):
77
+ out["exponent"] = scale.continuous.pow.exponent
78
+ if (
79
+ scale.continuous.symlog is not None
80
+ and scale.continuous.symlog.constant is not None
81
+ ):
82
+ out["constant"] = scale.continuous.symlog.constant
83
+ if include_encoding_fields:
84
+ out.update(map_fields(scale, SCALE_ENCODING_FIELD_MAP))
85
+ if scale.continuous is not None:
86
+ domain = scale.continuous.domain
87
+ if domain is not None:
88
+ out["domain"] = domain
89
+ if include_x_only:
90
+ if scale.x_reverse is not None:
91
+ out["xReverse"] = scale.x_reverse
92
+ return out
56
93
 
57
94
 
58
95
  def _n(obj: Any, *path: str) -> Any:
@@ -97,9 +134,9 @@ def axis_to_vl(
97
134
  ) -> dict[str, Any]:
98
135
  """Map any axis style object → VL axis config dict (non-None fields only).
99
136
 
100
- Works for AxisStyle (all fields concrete, always emitted) and
101
- AxisStylePatch (skip None). Single canonical mapper for all axis
102
- variants — theme global/channel/type-conditional and chart-local patches.
137
+ Works for any concrete axis style (all fields emitted) and any axis-variant
138
+ patch (skip None). Single canonical mapper for all axis variants — theme
139
+ global/channel/type-conditional and chart-local patches.
103
140
 
104
141
  ``label_overlap`` is the render-local VL directive from resolve_axis_x_overlap;
105
142
  it is not stored on the resolved model (VL vocabulary stays out of Resolved*).
@@ -108,27 +145,25 @@ def axis_to_vl(
108
145
  return {}
109
146
  d: dict[str, Any] = {}
110
147
 
111
- # Label font (nested via label.font)
112
- if (v := _n(axis, "label", "font", "color")) is not None:
148
+ # Label font (nested via labels.font)
149
+ if (v := _n(axis, "labels", "font", "color")) is not None:
113
150
  d["labelColor"] = v
114
- if (v := _n(axis, "label", "font", "family")) is not None:
151
+ if (v := _n(axis, "labels", "font", "family")) is not None:
115
152
  d["labelFont"] = v
116
- if (v := _n(axis, "label", "font", "size")) is not None:
153
+ if (v := _n(axis, "labels", "font", "size")) is not None:
117
154
  d["labelFontSize"] = v
118
- if (v := _n(axis, "label", "font", "weight")) is not None:
155
+ if (v := _n(axis, "labels", "font", "weight")) is not None:
119
156
  d["labelFontWeight"] = v
120
- if (v := _n(axis, "label", "padding")) is not None:
157
+ if (v := _n(axis, "labels", "padding")) is not None:
121
158
  d["labelPadding"] = v
122
- if (v := _n(axis, "label", "max_width")) is not None:
159
+ if (v := _n(axis, "labels", "max_width")) is not None:
123
160
  d["labelLimit"] = v
124
161
  if label_angle is not None:
125
162
  d["labelAngle"] = label_angle
126
- elif (v := _n(axis, "label", "angle")) is not None:
163
+ elif (v := _n(axis, "labels", "angle")) is not None:
127
164
  d["labelAngle"] = v
128
- if (v := _n(axis, "label", "align")) is not None:
165
+ if (v := _n(axis, "labels", "align")) is not None:
129
166
  d["labelAlign"] = v
130
- if (v := _n(axis, "label", "baseline")) is not None:
131
- d["labelBaseline"] = v
132
167
  # label_overlap is the render-local VL directive from resolve_axis_x_overlap.
133
168
  # "allow" → false (no reduction; labels may overlap).
134
169
  # "parity" → pass through as-is (VL drops every other label).
@@ -137,22 +172,20 @@ def axis_to_vl(
137
172
  d["labelOverlap"] = False
138
173
  elif label_overlap == "parity":
139
174
  d["labelOverlap"] = label_overlap
140
- if (v := _n(axis, "label", "separation")) is not None:
175
+ if (v := _n(axis, "labels", "min_gap")) is not None:
141
176
  d["labelSeparation"] = v
142
- if (v := _n(axis, "label", "visible")) is not None:
177
+ if (v := _n(axis, "labels", "visible")) is not None:
143
178
  d["labels"] = v
144
- if (v := _n(axis, "label", "expr")) is not None:
179
+ if (v := _n(axis, "labels", "expr")) is not None:
145
180
  d["labelExpr"] = v
146
- if (v := _n(axis, "label", "bound")) is not None:
181
+ if (v := _n(axis, "labels", "bound")) is not None:
147
182
  d["labelBound"] = v
148
- if (v := _n(axis, "label", "flush")) is not None:
183
+ if (v := _n(axis, "labels", "flush")) is not None:
149
184
  d["labelFlush"] = v
150
- if (v := _n(axis, "label", "offset")) is not None:
185
+ if (v := _n(axis, "labels", "offset")) is not None:
151
186
  d["labelOffset"] = v
152
- if (v := _n(axis, "label", "line_height")) is not None:
153
- d["labelLineHeight"] = v
154
- if (v := _n(axis, "label", "anchor")) is not None:
155
- d["labelAnchor"] = v
187
+ if (v := _n(axis, "labels", "format")) is not None:
188
+ d["format"] = v
156
189
 
157
190
  # Title: null suppresses the visual axis label (title still appears in tooltips
158
191
  # via encoding.title). Emitting null is the correct VL mechanism — it removes
@@ -184,12 +217,12 @@ def axis_to_vl(
184
217
  if (v := _n(axis, "grid", "dash")) is not None:
185
218
  d["gridDash"] = v
186
219
 
187
- # Domain
188
- if (v := _n(axis, "domain", "visible")) is not None:
220
+ # Domain (Dataface's `line` field — VL's axis.domain* properties)
221
+ if (v := _n(axis, "line", "visible")) is not None:
189
222
  d["domain"] = v
190
- if (v := _n(axis, "domain", "width")) is not None:
223
+ if (v := _n(axis, "line", "width")) is not None:
191
224
  d["domainWidth"] = v
192
- if (v := _n(axis, "domain", "color")) is not None:
225
+ if (v := _n(axis, "line", "color")) is not None:
193
226
  d["domainColor"] = v
194
227
 
195
228
  # Ticks
@@ -197,21 +230,19 @@ def axis_to_vl(
197
230
  d["ticks"] = v
198
231
  if (v := _n(axis, "ticks", "color")) is not None:
199
232
  d["tickColor"] = v
200
- if (v := _n(axis, "ticks", "size")) is not None:
233
+ if (v := _n(axis, "ticks", "length")) is not None:
201
234
  d["tickSize"] = v
202
235
  if (v := _n(axis, "ticks", "width")) is not None:
203
236
  d["tickWidth"] = v
237
+ if (v := _n(axis, "ticks", "offset")) is not None:
238
+ d["offset"] = v
204
239
 
205
240
  # Flat fields
206
241
  if (v := _n(axis, "position")) is not None:
207
242
  d["orient"] = v
208
- if (v := _n(axis, "offset")) is not None:
209
- d["offset"] = v
210
243
  if (v := _n(axis, "band_position")) is not None:
211
244
  d["bandPosition"] = v
212
- if (v := _n(axis, "format")) is not None:
213
- d["format"] = v
214
- if (v := _n(axis, "values")) is not None:
245
+ if (v := _n(axis, "scale", "values")) is not None:
215
246
  d["values"] = v
216
247
 
217
248
  return d
@@ -262,28 +293,32 @@ def measure_axis_to_vl(
262
293
  orient in ("left", "right")
263
294
  and align == orient
264
295
  and "labelExpr" not in d
265
- and axis.label.font.case not in ("upper", "lower")
296
+ and axis.labels.font.case not in ("upper", "lower")
266
297
  ):
267
298
  if category_labels:
268
299
  labels = list(category_labels)
269
- elif axis.format is not None:
300
+ elif axis.labels.format is not None:
270
301
  if axis.tick_values:
271
- labels = quantitative_tick_labels(tuple(axis.tick_values), axis.format)
302
+ labels = quantitative_tick_labels(
303
+ tuple(axis.tick_values), axis.labels.format
304
+ )
272
305
  else:
273
- domain = axis.scale.domain if axis.scale is not None else None
306
+ _sc = axis.scale
307
+ _sc_cont = _sc.continuous if _sc is not None else None
308
+ domain = _sc_cont.domain if _sc_cont is not None else None
274
309
  bounds = numeric_domain_bounds(domain)
275
310
  values = numeric_values(data, y_fields) + (
276
311
  list(bounds) if bounds is not None else []
277
312
  )
278
- labels = estimated_quantitative_tick_labels(values, axis.format)
313
+ labels = estimated_quantitative_tick_labels(values, axis.labels.format)
279
314
  if labels:
280
315
  tick_size = (
281
- axis.ticks.size if axis.ticks.size is not None else DEFAULT_VL_TICK_SIZE
316
+ axis.ticks.length if axis.ticks.length is not None else DEFAULT_VL_TICK_SIZE
282
317
  )
283
- padding = measured_label_padding(labels, axis.label.font, tick_size)
318
+ padding = measured_label_padding(labels, axis.labels.font, tick_size)
284
319
  label_limit = (
285
- axis.label.max_width
286
- if axis.label.max_width is not None
320
+ axis.labels.max_width
321
+ if axis.labels.max_width is not None
287
322
  else DEFAULT_VL_LABEL_LIMIT
288
323
  )
289
324
  d["labelPadding"] = cap_padding_to_label_limit(padding, label_limit, tick_size)
@@ -329,7 +364,7 @@ def inject_axis_label_case(ax_vl: dict[str, Any], axis: Any) -> dict[str, Any]:
329
364
  MUST be called AFTER build_cartesian_x_encoding for x-axes so the temporal
330
365
  smart-cadence labelExpr is already set before wrapping.
331
366
  """
332
- case = _n(axis, "label", "font", "case")
367
+ case = _n(axis, "labels", "font", "case")
333
368
  if case not in ("upper", "lower"):
334
369
  return ax_vl
335
370
  inner = ax_vl.get("labelExpr", "datum.label")
@@ -337,21 +372,21 @@ def inject_axis_label_case(ax_vl: dict[str, Any], axis: Any) -> dict[str, Any]:
337
372
 
338
373
 
339
374
  def inject_axis_label_values_filter(ax_vl: dict[str, Any], axis: Any) -> dict[str, Any]:
340
- """Wrap labelExpr with a membership filter for authored ``label.values``.
375
+ """Wrap labelExpr with a membership filter for authored ``labels.values``.
341
376
 
342
- Blanks any tick whose date isn't in ``axis.label.values``, decoupling
377
+ Blanks any tick whose date isn't in ``axis.labels.values``, decoupling
343
378
  label density from tick/grid density: tick/grid values are left
344
- dense (auto-filled or explicitly authored via ``axis.values``); only the
345
- listed dates keep their label text. The inner expression defaults to
379
+ dense (auto-filled or explicitly authored via ``axis.scale.values``); only
380
+ the listed dates keep their label text. The inner expression defaults to
346
381
  ``datum.label`` when no labelExpr is present yet, so authoring only
347
- ``label.values`` on an otherwise-plain temporal axis still respects
382
+ ``labels.values`` on an otherwise-plain temporal axis still respects
348
383
  whatever ``format`` VL already applied to ``datum.label``.
349
384
 
350
385
  MUST be called AFTER build_cartesian_x_encoding / inject_axis_label_case
351
386
  so it wraps whatever label text (smart cadence, custom format, case) those
352
- steps already produced — label.values always wins on top.
387
+ steps already produced — labels.values always wins on top.
353
388
  """
354
- label_values = _n(axis, "label", "values")
389
+ label_values = _n(axis, "labels", "values")
355
390
  if label_values is None:
356
391
  return ax_vl
357
392
  from dataface.core.render.chart.time_unit_detect import label_values_filter_expr
@@ -0,0 +1,122 @@
1
+ """Boolean conditions authored in YAML — ``enabled``, ``visible``.
2
+
3
+ Stage: RENDER
4
+ Purpose: Turn a bool, a Jinja expression, or a single-row query probe into the
5
+ yes/no a renderer needs.
6
+
7
+ Three surfaces share this: a variable's ``enabled``, a layout item's
8
+ ``visible``, and the terminal renderer's copy of the same question. They differ
9
+ only in what a missing condition means, which is why ``if_none`` is a parameter
10
+ rather than a default baked in here.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import TYPE_CHECKING, Any
16
+
17
+ from dataface.core.compile.jinja import resolve_jinja_template
18
+
19
+ if TYPE_CHECKING:
20
+ from dataface.core.compile.models.variable.authored import SingleRowBoolProbe
21
+ from dataface.core.execute.chart_data_provider import ChartDataProvider
22
+
23
+
24
+ def coerce_bool(value: Any) -> bool:
25
+ """Coerce value to bool; raise ValueError for unrecognized inputs."""
26
+ if isinstance(value, bool):
27
+ return value
28
+ if isinstance(value, (int, float)):
29
+ if value == 1:
30
+ return True
31
+ if value == 0:
32
+ return False
33
+ if isinstance(value, str):
34
+ lower = value.strip().lower()
35
+ if lower in ("true", "1", "yes"):
36
+ return True
37
+ if lower in ("false", "0", "no"):
38
+ return False
39
+ raise ValueError(f"Cannot coerce {value!r} to bool; expected true/false/1/0/yes/no")
40
+
41
+
42
+ def eval_bool_condition(
43
+ expr: bool | str | SingleRowBoolProbe | None,
44
+ if_none: bool,
45
+ current_values: dict[str, Any],
46
+ executor: ChartDataProvider | None,
47
+ context: str,
48
+ ) -> bool:
49
+ """Evaluate a bool/str/probe condition and return the result.
50
+
51
+ Shared core for Variable.enabled and LayoutItem.visible; callers pass
52
+ ``if_none`` to control the meaning of a missing (None) value.
53
+
54
+ - None: return ``if_none`` (enabled → True, visible → True).
55
+ - bool: return as-is.
56
+ - str: auto-wrap in ``{{ }}`` if needed, evaluate as Jinja, coerce to bool.
57
+ - SingleRowBoolProbe: execute the named query; expect exactly 1 row; coerce
58
+ the named column to bool.
59
+
60
+ Args:
61
+ expr: The condition value (from YAML or a compiled model field).
62
+ if_none: Value to return when expr is None.
63
+ current_values: Current variable values for Jinja resolution.
64
+ executor: Required when expr is a SingleRowBoolProbe.
65
+ context: Human-readable label for error messages (e.g. "Variable 'region' disabled").
66
+
67
+ Raises:
68
+ ValueError: Wrong row count, missing column, non-bool-coercible value, or
69
+ no executor for a query-backed condition.
70
+ """
71
+ if expr is None:
72
+ return if_none
73
+ if isinstance(expr, bool):
74
+ return expr
75
+ if isinstance(expr, str):
76
+ # Auto-wrap bare variable names / Jinja expressions (no {{ }} required).
77
+ jinja_expr = expr if "{{" in expr else f"{{{{ {expr} }}}}"
78
+ rendered = resolve_jinja_template(jinja_expr, variables=current_values)
79
+ return coerce_bool(rendered)
80
+ if executor is None:
81
+ raise ValueError(
82
+ f"{context} query '{expr.query}' requires an executor but none was provided"
83
+ )
84
+ rows = executor.execute_query(expr.query, current_values)
85
+ if not rows:
86
+ raise ValueError(
87
+ f"{context} query '{expr.query}' returned no rows; expected exactly 1"
88
+ )
89
+ if len(rows) > 1:
90
+ raise ValueError(
91
+ f"{context} query '{expr.query}' returned {len(rows)} rows; expected exactly 1"
92
+ )
93
+ row = rows[0]
94
+ if expr.column not in row:
95
+ raise ValueError(
96
+ f"{context} query '{expr.query}' has no column '{expr.column}'; "
97
+ f"available: {list(row.keys())}"
98
+ )
99
+ return coerce_bool(row[expr.column])
100
+
101
+
102
+ def evaluate_visible(
103
+ expr: bool | str | SingleRowBoolProbe | None,
104
+ current_values: dict[str, Any],
105
+ executor: ChartDataProvider | None,
106
+ context: str = "layout item",
107
+ ) -> bool:
108
+ """Return True when the layout item should be rendered.
109
+
110
+ Args:
111
+ expr: The visible value from the layout item.
112
+ current_values: Current variable values for Jinja resolution.
113
+ executor: Required when expr is a SingleRowBoolProbe.
114
+ context: Human-readable label for error messages (e.g. "layout item 'kpi_a'").
115
+ """
116
+ return eval_bool_condition(
117
+ expr,
118
+ True,
119
+ current_values,
120
+ executor,
121
+ f"{context} visible",
122
+ )