dataface 0.2.1.dev490__py3-none-any.whl → 0.2.1.dev516__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 (53) hide show
  1. dataface/agent_api/docs/yaml-reference.md +24 -7
  2. dataface/agent_api/project_session.py +1 -24
  3. dataface/core/compile/compiler.py +25 -0
  4. dataface/core/compile/errors.py +16 -3
  5. dataface/core/compile/models/chart/normalized/_base.py +2 -2
  6. dataface/core/compile/models/chart/normalized/callout.py +2 -2
  7. dataface/core/compile/models/face/normalized.py +4 -0
  8. dataface/core/compile/models/face/resolved.py +1 -0
  9. dataface/core/compile/models/primitives.py +0 -10
  10. dataface/core/compile/models/style/resolved/_base.py +5 -4
  11. dataface/core/compile/models/style/resolved/_cascade.py +6 -7
  12. dataface/core/compile/models/style/theme/__init__.py +2 -0
  13. dataface/core/compile/models/style/theme/axis.py +11 -12
  14. dataface/core/compile/models/style/theme/board.py +4 -0
  15. dataface/core/compile/models/style/theme/charts.py +3 -3
  16. dataface/core/compile/models/style/theme/legend.py +11 -0
  17. dataface/core/compile/models/style/theme/marks.py +18 -6
  18. dataface/core/compile/normalize_charts.py +14 -4
  19. dataface/core/compile/normalize_layout.py +36 -5
  20. dataface/core/compile/resolve/_resolve.py +65 -18
  21. dataface/core/compile/typography.py +26 -15
  22. dataface/core/defaults/themes/_base.yaml +10 -10
  23. dataface/core/errors/execution.py +11 -2
  24. dataface/core/errors/structured.py +1 -1
  25. dataface/core/execute/adapters/base.py +2 -0
  26. dataface/core/execute/executor.py +19 -21
  27. dataface/core/render/chart/callout.py +13 -24
  28. dataface/core/render/chart/data_table_attachment.py +195 -106
  29. dataface/core/render/chart/emitters/_cartesian.py +8 -2
  30. dataface/core/render/chart/emitters/_label_overlap.py +162 -235
  31. dataface/core/render/chart/emitters/_measured_label_padding.py +1 -1
  32. dataface/core/render/chart/emitters/bar.py +3 -3
  33. dataface/core/render/chart/emitters/heatmap.py +1 -1
  34. dataface/core/render/chart/rendering.py +44 -2
  35. dataface/core/render/chart/time_unit_detect.py +336 -54
  36. dataface/core/render/chart/type_inference.py +103 -31
  37. dataface/core/render/chart/vl_field_maps.py +2 -0
  38. dataface/core/render/comment_stripping.py +37 -0
  39. dataface/core/render/face_resolve.py +2 -0
  40. dataface/core/render/faces.py +53 -8
  41. dataface/core/render/layouts.py +16 -0
  42. dataface/core/render/script_embedding.py +9 -1
  43. dataface/core/render/sizing.py +22 -4
  44. dataface/core/render/templates/controls/_styles.css +92 -96
  45. dataface/core/render/templates/scripts/chart_interactivity.js +174 -174
  46. dataface/core/render/templates/scripts/variables.js +205 -208
  47. dataface/core/render/templates/svg/styles.css +12 -13
  48. dataface/core/render/warnings/value_labels_crowd_width.py +3 -3
  49. {dataface-0.2.1.dev490.dist-info → dataface-0.2.1.dev516.dist-info}/METADATA +1 -1
  50. {dataface-0.2.1.dev490.dist-info → dataface-0.2.1.dev516.dist-info}/RECORD +53 -52
  51. {dataface-0.2.1.dev490.dist-info → dataface-0.2.1.dev516.dist-info}/WHEEL +0 -0
  52. {dataface-0.2.1.dev490.dist-info → dataface-0.2.1.dev516.dist-info}/entry_points.txt +0 -0
  53. {dataface-0.2.1.dev490.dist-info → dataface-0.2.1.dev516.dist-info}/licenses/LICENSE +0 -0
@@ -1077,7 +1077,6 @@ Authored overlay for RootFontStyle. Root-level font configuration: FontStyle fie
1077
1077
  | `decoration` | enum: "none", "line-through", "underline" | ✓ | Text decoration (underline, line-through, or none). |
1078
1078
  | `case` | enum: "none", "sentence", "title", "upper", "lower", "slug", "camel" | ✓ | Letter-case transform applied at render time. 'title' uses Chicago/Gruber rules and preserves tokens with internal capitals (ARR, iPhone). 'sentence' uppercases only the first character. 'none' (default) emits the string without any letter-case change. |
1079
1079
  | `line_height` | float | ✓ | Line height as a unitless multiple of font size. Cascades through Style.font to all text roles that carry a FontStyle slot. Body prose defaults to 1.25; titles override tighter via their font patch in theme YAML. |
1080
- | `min_size` | float | ✓ | Minimum font size in pixels; used by the axis label shrink strategy. |
1081
1080
  | `emoji` | enum: "monochrome", "color", "system-default", "disabled" | ✓ | Emoji rendering mode for the dashboard font stack. |
1082
1081
 
1083
1082
  <a id="borderstyle"></a>
@@ -1097,6 +1096,7 @@ Authored overlay for TitleStyle. Board and face titles.
1097
1096
  | Field | Type | Optional | Description |
1098
1097
  |-------|------|:--------:|-------------|
1099
1098
  | `font` | [FontStyle](#fontstyle) | ✓ | Title font style overrides. Unset fields fall back to [`style.font`](#style). |
1099
+ | `compact_weight` | str \| float | ✓ | Object-title font weight on tiny cards. |
1100
1100
  | `sizes` | list[float] | ✓ | Font sizes for the H1–H6 heading ramp, indexed by ``face.level - 1``. Face/prose titles index this ramp directly (no width input). Object titles (chart/table/spark) combine ``width_offsets`` with a fixed object-title anchor to pick a slot. |
1101
1101
  | `width_offsets` | [TitleWidthOffsetsStyle](#titlewidthoffsetsstyle) | ✓ | Additive level offsets by card width (tiny/narrow/medium/wide). Consumed by ``chart_title_spec`` only — object titles add the tier offset to a fixed anchor to pick an H slot from ``sizes``. Face/prose titles are level-only and do not consult these. |
1102
1102
  | `min_height` | float | ✓ | Minimum title row height in pixels. |
@@ -1166,7 +1166,7 @@ Authored overlay for ChartsStyle. Registry of all chart-type styles plus shared
1166
1166
  | `geoshape` | [GeoshapeChartStyle](#geoshapechartstyle) | ✓ | Geoshape (choropleth) chart style. |
1167
1167
  | `point_map` | [PointMapChartStyle](#pointmapchartstyle) | ✓ | Point map chart style. |
1168
1168
  | `pie` | [PieChartStyle](#piechartstyle) | ✓ | Pie/donut chart style. |
1169
- | `series_label` | [SeriesLabelStyle](#serieslabelstyle) | ✓ | Shared series-label typography for endpoint labels, stack labels, and slice callouts. |
1169
+ | `series_label` | [SeriesLabelStyle](#serieslabelstyle) | ✓ | Shared series-label typography for endpoint and stack labels. |
1170
1170
  | `kpi` | [KpiChartStyle](#kpichartstyle) | ✓ | KPI card chart style. |
1171
1171
  | `table` | [TableChartStyle](#tablechartstyle) | ✓ | Table chart style. |
1172
1172
  | `spark_bar` | [SparkBarChartStyle](#sparkbarchartstyle) | ✓ | Spark_bar (full-chart horizontal bar) style. |
@@ -1323,7 +1323,6 @@ Text appearance. Merged as a unit.
1323
1323
  | `decoration` | enum: "none", "line-through", "underline" | ✓ | Text decoration (underline, line-through, or none). |
1324
1324
  | `case` | enum: "none", "sentence", "title", "upper", "lower", "slug", "camel" | ✓ | Letter-case transform applied at render time. 'title' uses Chicago/Gruber rules and preserves tokens with internal capitals (ARR, iPhone). 'sentence' uppercases only the first character. 'none' (default) emits the string without any letter-case change. |
1325
1325
  | `line_height` | float | ✓ | Line height as a unitless multiple of font size. Cascades through Style.font to all text roles that carry a FontStyle slot. Body prose defaults to 1.25; titles override tighter via their font patch in theme YAML. |
1326
- | `min_size` | float | ✓ | Minimum font size in pixels; used by the axis label shrink strategy. |
1327
1326
 
1328
1327
  <a id="paddingstyle"></a>
1329
1328
  ## PaddingStyle
@@ -1344,6 +1343,8 @@ Authored overlay for LegendStyle.
1344
1343
  |-------|------|:--------:|-------------|
1345
1344
  | `position` | str | ✓ | Legend position (e.g. 'right', 'bottom'). |
1346
1345
  | `direction` | str | ✓ | Legend layout direction ('vertical' or 'horizontal'). |
1346
+ | `columns` | int | ✓ | Legend entry columns. Zero keeps the renderer default; positive values set Vega-Lite legend columns. |
1347
+ | `compact_columns` | int | ✓ | Entry columns for an automatic compact top-horizontal legend. |
1347
1348
  | `label` | [LegendLabelStyle](#legendlabelstyle) | ✓ | Legend label style. |
1348
1349
  | `title` | [LegendTitleStyle](#legendtitlestyle) | ✓ | Legend title style. |
1349
1350
  | `visible` | bool | ✓ | Show the legend. None = legend visible; False = explicitly suppressed. |
@@ -2006,7 +2007,7 @@ Authored overlay for SeriesLabelStyle. Series-label primitive — typography for
2006
2007
 
2007
2008
  | Field | Type | Optional | Description |
2008
2009
  |-------|------|:--------:|-------------|
2009
- | `font` | [FontStyle](#fontstyle) | ✓ | Series label font style overrides; cascade fills missing fields from charts.font. Unset fields fall back to [`style.charts.font`](#chartsstyle). |
2010
+ | `font` | [SeriesLabelFontStyle](#serieslabelfontstyle) | ✓ | Series label font style overrides; cascade fills missing fields from charts.font. Unset fields fall back to [`style.charts.font`](#chartsstyle). |
2010
2011
 
2011
2012
  <a id="layoutgapstyle"></a>
2012
2013
  ## LayoutGapStyle
@@ -2227,7 +2228,7 @@ Authored overlay for AxisElementStyle. Axis label or title: font + padding + VL-
2227
2228
  | `angle` | float | ✓ | Label rotation angle in degrees; None uses Vega-Lite's default. |
2228
2229
  | `align` | str | ✓ | Horizontal text alignment of labels; None uses Vega-Lite's default. |
2229
2230
  | `baseline` | str | ✓ | Vertical text baseline of labels; None uses Vega-Lite's default. |
2230
- | `overlap` | [AxisLabelOverlapConfig](#axislabeloverlapconfig) | ✓ | Label overlap strategy enablement. None inherits from the theme cascade. Set individual bools to enable/disable; the resolver applies them in fixed order shrink→tilt→skip. |
2231
+ | `overlap` | [AxisLabelOverlapConfig](#axislabeloverlapconfig) | ✓ | Label overlap strategy enablement. None inherits from the theme cascade. Set individual bools to enable/disable; the resolver applies them in fixed order tilt→skip. Temporal-bucketed axes ignore this and step the label cadence instead. |
2231
2232
  | `separation` | float | ✓ | Minimum pixel separation between labels; None uses Vega-Lite's default (0px). |
2232
2233
  | `visible` | bool | ✓ | Show axis labels; None uses Vega-Lite's default (labels shown). |
2233
2234
  | `time_unit` | enum: "auto", "year", "yearquarter", "yearmonth", "yearweek", "yearmonthdate", "monthofyear", "dayofweek", "dayofmonth", "dayofyear", "hourofday", "none" | ✓ | Label cadence for temporal axes; None inherits from the parent axis time_unit. |
@@ -2584,6 +2585,23 @@ Authored overlay for HistogramChartMarksStyle. Histogram-family mark overrides.
2584
2585
  | `bar` | [BarMarkStyle](#barmarkstyle) | ✓ | Bar mark overrides; inherits from global. |
2585
2586
  | `rule` | [RuleMarkStyle](#rulemarkstyle) | ✓ | Rule mark overrides; None inherits global. |
2586
2587
 
2588
+ <a id="serieslabelfontstyle"></a>
2589
+ ## SeriesLabelFontStyle
2590
+ Authored overlay for SeriesLabelFontStyle. Series-label typography with a compact width-tier size.
2591
+
2592
+ | Field | Type | Optional | Description |
2593
+ |-------|------|:--------:|-------------|
2594
+ | `family` | str | ✓ | Font family name (e.g., 'sans-serif', 'Roboto'). |
2595
+ | `color` | str | ✓ | Text color as a CSS color string. |
2596
+ | `size` | float | ✓ | Font size in pixels. |
2597
+ | `weight` | str \| float | ✓ | Font weight (e.g., 'bold', 400, 700). |
2598
+ | `style` | enum: "normal", "italic" | ✓ | Font style (normal or italic). |
2599
+ | `decoration` | enum: "none", "line-through", "underline" | ✓ | Text decoration (underline, line-through, or none). |
2600
+ | `case` | enum: "none", "sentence", "title", "upper", "lower", "slug", "camel" | ✓ | Letter-case transform applied at render time. 'title' uses Chicago/Gruber rules and preserves tokens with internal capitals (ARR, iPhone). 'sentence' uppercases only the first character. 'none' (default) emits the string without any letter-case change. |
2601
+ | `line_height` | float | ✓ | Line height as a unitless multiple of font size. Cascades through Style.font to all text roles that carry a FontStyle slot. Body prose defaults to 1.25; titles override tighter via their font patch in theme YAML. |
2602
+ | `compact_size` | float | ✓ | Series-label font size in pixels on tiny and narrow cards. |
2603
+ | `compact_weight` | str \| float | ✓ | Series-label font weight on tiny and narrow cards. |
2604
+
2587
2605
  <a id="detailsarrowstyle"></a>
2588
2606
  ## DetailsArrowStyle
2589
2607
  Authored overlay for DetailsArrowStyle. Layout and font style for the expand/collapse arrow chevron.
@@ -2627,11 +2645,10 @@ Authored overlay for AxisGridZeroStyle. Zero-baseline gridline color and width o
2627
2645
 
2628
2646
  <a id="axislabeloverlapconfig"></a>
2629
2647
  ## AxisLabelOverlapConfig
2630
- Authored overlay for AxisLabelOverlapConfig. Three-bool overlap strategy enablement for axis labels.
2648
+ Authored overlay for AxisLabelOverlapConfig. Two-bool overlap strategy enablement for CATEGORICAL axis labels.
2631
2649
 
2632
2650
  | Field | Type | Optional | Description |
2633
2651
  |-------|------|:--------:|-------------|
2634
- | `shrink` | bool | ✓ | Enable font-size shrink strategy; None inherits from parent axis. |
2635
2652
  | `tilt` | bool | ✓ | Enable label tilt strategy; None inherits from parent axis. |
2636
2653
  | `skip` | bool | ✓ | Enable parity-skip strategy (drops every other label); None inherits from parent axis. |
2637
2654
 
@@ -36,7 +36,6 @@ from dataface.core.execute.adapters.adapter_registry import (
36
36
  build_adapter_registry,
37
37
  )
38
38
  from dataface.core.execute.cache_backend import QueryResultCache
39
- from dataface.core.execute.observability import WarehouseObserver
40
39
  from dataface.core.inspect.query_validator import (
41
40
  QueryDiagnostic,
42
41
  validate_query as _core_validate_query,
@@ -75,11 +74,9 @@ class ProjectSession:
75
74
  _read_only: bool
76
75
  _connection_string: str | None
77
76
  _dialect: str
78
- _target: str
79
77
  _duckdb_config: dict[str, Any] | None
80
78
  _allow_external_access_in_readonly: bool
81
79
  _resolver: SourceResolver | None
82
- _observers: list[WarehouseObserver]
83
80
  _file_materializer: FileSourceMaterializer | None
84
81
 
85
82
  def __init__(
@@ -88,7 +85,6 @@ class ProjectSession:
88
85
  cache: QueryResultCache | None = None,
89
86
  adapter_registry: AdapterRegistry | None = None,
90
87
  read_only: bool = True,
91
- observers: list[WarehouseObserver] | None = None,
92
88
  file_materializer: FileSourceMaterializer | None = None,
93
89
  ) -> None:
94
90
  self.project = project
@@ -103,11 +99,9 @@ class ProjectSession:
103
99
  self._read_only = read_only
104
100
  self._connection_string = None
105
101
  self._dialect = "duckdb"
106
- self._target = "dev"
107
102
  self._duckdb_config = None
108
103
  self._allow_external_access_in_readonly = False
109
104
  self._resolver = None
110
- self._observers = list(observers) if observers else []
111
105
  self._file_materializer = file_materializer
112
106
 
113
107
  @classmethod
@@ -119,11 +113,9 @@ class ProjectSession:
119
113
  read_only: bool = True,
120
114
  connection_string: str | None = None,
121
115
  dialect: str = "duckdb",
122
- target: str = "dev",
123
116
  duckdb_config: dict[str, Any] | None = None,
124
117
  allow_external_access_in_readonly: bool = False,
125
118
  resolver: SourceResolver | None = None,
126
- observers: list[WarehouseObserver] | None = None,
127
119
  ) -> Self:
128
120
  """Construct a ProjectSession rooted at *project_dir*.
129
121
 
@@ -142,11 +134,9 @@ class ProjectSession:
142
134
  project=FilesystemProject(Path(project_dir).resolve()),
143
135
  cache=cache,
144
136
  read_only=read_only,
145
- observers=observers,
146
137
  )
147
138
  session._connection_string = connection_string
148
139
  session._dialect = dialect
149
- session._target = target
150
140
  session._duckdb_config = duckdb_config
151
141
  session._allow_external_access_in_readonly = allow_external_access_in_readonly
152
142
  session._resolver = resolver
@@ -160,13 +150,8 @@ class ProjectSession:
160
150
  cache: QueryResultCache | None = None,
161
151
  adapter_registry: AdapterRegistry | None = None,
162
152
  read_only: bool = True,
163
- connection_string: str | None = None,
164
- dialect: str = "duckdb",
165
- target: str = "dev",
166
153
  duckdb_config: dict[str, Any] | None = None,
167
154
  allow_external_access_in_readonly: bool = False,
168
- resolver: SourceResolver | None = None,
169
- observers: list[WarehouseObserver] | None = None,
170
155
  file_materializer: FileSourceMaterializer | None = None,
171
156
  ) -> Self:
172
157
  """Construct a ProjectSession from a pre-built Project.
@@ -185,15 +170,10 @@ class ProjectSession:
185
170
  cache=cache,
186
171
  adapter_registry=adapter_registry,
187
172
  read_only=read_only,
188
- observers=observers,
189
173
  file_materializer=file_materializer,
190
174
  )
191
- session._connection_string = connection_string
192
- session._dialect = dialect
193
- session._target = target
194
175
  session._duckdb_config = duckdb_config
195
176
  session._allow_external_access_in_readonly = allow_external_access_in_readonly
196
- session._resolver = resolver
197
177
  return session
198
178
 
199
179
  @classmethod
@@ -201,13 +181,12 @@ class ProjectSession:
201
181
  cls,
202
182
  face_file: Path | str,
203
183
  *,
204
- read_only: bool = True,
205
184
  cache: QueryResultCache | None = None,
206
185
  ) -> Self:
207
186
  """Open a ProjectSession rooted at the project directory discovered upward from face_file."""
208
187
  resolved = Path(face_file).resolve()
209
188
  project_root = find_project_root(resolved.parent, boundary=None)
210
- return cls.open(project_root, read_only=read_only, cache=cache)
189
+ return cls.open(project_root, cache=cache)
211
190
 
212
191
  def __enter__(self) -> Self:
213
192
  return self
@@ -241,11 +220,9 @@ class ProjectSession:
241
220
  read_only=self._read_only,
242
221
  connection_string=self._connection_string,
243
222
  profile_type=self._dialect,
244
- target=self._target,
245
223
  duckdb_config=self._duckdb_config,
246
224
  allow_external_access_in_readonly=self._allow_external_access_in_readonly,
247
225
  resolver=self._resolver,
248
- observers=self._observers,
249
226
  )
250
227
 
251
228
  def refresh(self) -> None:
@@ -82,8 +82,10 @@ from dataface.core.compile.parser import (
82
82
  )
83
83
  from dataface.core.compile.validator import validate_face
84
84
  from dataface.core.compile.yaml_error_formatter import (
85
+ find_yaml_line_number,
85
86
  format_validation_errors_structured,
86
87
  )
88
+ from dataface.core.errors.codes_compile import DF_COMPILE_UNKNOWN_QUERY
87
89
  from dataface.core.errors.structured import StructuredError
88
90
  from dataface.core.warnings import unreferenced_chart
89
91
  from dataface.core.warnings.base import RenderWarning
@@ -187,6 +189,18 @@ def compile_authored_face(
187
189
  # ════════════════════════════════════════════════════════════════════
188
190
  validation_errors = validate_face(authored)
189
191
  if validation_errors:
192
+ for error in validation_errors:
193
+ if error.code is not DF_COMPILE_UNKNOWN_QUERY:
194
+ continue
195
+ chart_name = error.fields["chart_name"]
196
+ if authored.charts is None or chart_name not in authored.charts:
197
+ continue
198
+ ref_path = ["charts", chart_name, "query"]
199
+ line = find_yaml_line_number(_yaml_content, ref_path)
200
+ if line is None:
201
+ continue
202
+ error.line = line
203
+ error.field_path = ".".join(ref_path)
190
204
  return CompileResult(errors=[e.to_structured() for e in validation_errors])
191
205
 
192
206
  # ════════════════════════════════════════════════════════════════════
@@ -362,6 +376,17 @@ def compile_authored_face(
362
376
  errors.extend(e.structured_errors)
363
377
  return CompileResult(errors=errors)
364
378
  except ReferenceError as e:
379
+ if (
380
+ e.ref_path
381
+ and len(e.ref_path) >= 2
382
+ and e.ref_path[0] == "charts"
383
+ and authored.charts is not None
384
+ and e.ref_path[1] in authored.charts
385
+ ):
386
+ line = find_yaml_line_number(_yaml_content, list(e.ref_path))
387
+ if line is not None:
388
+ e.line = line
389
+ e.field_path = ".".join(e.ref_path)
365
390
  return CompileResult(errors=[e.to_structured()])
366
391
  except CompilationError as e:
367
392
  return CompileResult(errors=[e.to_structured()])
@@ -21,6 +21,7 @@ Refs #94
21
21
 
22
22
  from __future__ import annotations
23
23
 
24
+ from collections.abc import Sequence
24
25
  from typing import Any
25
26
 
26
27
  from dataface.core.errors.base import DatafaceError
@@ -37,6 +38,7 @@ class CompilationError(DatafaceError):
37
38
  location: Optional location in source (line number, path, etc.)
38
39
  line: Optional line number in the YAML file
39
40
  column: Optional column number in the YAML file
41
+ field_path: Optional dotted path to the field that failed
40
42
  context: Optional YAML snippet showing the error location
41
43
  suggestion: Optional helpful suggestion for fixing the error
42
44
  """
@@ -48,6 +50,7 @@ class CompilationError(DatafaceError):
48
50
  location: str | None = None
49
51
  line: int | None = None
50
52
  column: int | None = None
53
+ field_path: str = ""
51
54
  context: str | None = None
52
55
  suggestion: str | None = None
53
56
 
@@ -57,6 +60,7 @@ class CompilationError(DatafaceError):
57
60
  location: str | None = None,
58
61
  line: int | None = None,
59
62
  column: int | None = None,
63
+ field_path: str = "",
60
64
  context: str | None = None,
61
65
  suggestion: str | None = None,
62
66
  ):
@@ -64,6 +68,7 @@ class CompilationError(DatafaceError):
64
68
  self.location = location
65
69
  self.line = line
66
70
  self.column = column
71
+ self.field_path = field_path
67
72
  self.context = context
68
73
  self.suggestion = suggestion
69
74
  self.fields: dict[str, Any] = {}
@@ -132,7 +137,7 @@ class ValidationError(CompilationError):
132
137
  ... print(f"Validation failed at line {e.line}: {e}")
133
138
  """
134
139
 
135
- field_path: str | None = None
140
+ field_path: str = ""
136
141
  invalid_value: str | None = None
137
142
  valid_values: list[str] | None = None
138
143
 
@@ -147,7 +152,7 @@ class ValidationError(CompilationError):
147
152
  invalid_value: str | None = None,
148
153
  valid_values: list[str] | None = None,
149
154
  ):
150
- self.field_path = field_path
155
+ self.field_path = field_path if field_path is not None else ""
151
156
  self.invalid_value = invalid_value
152
157
  self.valid_values = valid_values
153
158
 
@@ -155,6 +160,7 @@ class ValidationError(CompilationError):
155
160
  message,
156
161
  location=location if not context else None,
157
162
  line=line,
163
+ field_path=field_path if field_path is not None else "",
158
164
  context=context,
159
165
  suggestion=suggestion,
160
166
  )
@@ -181,11 +187,18 @@ class ReferenceError(CompilationError):
181
187
  """
182
188
 
183
189
  ref: str | None = None
190
+ ref_path: tuple[str, ...] = ()
184
191
 
185
- def __init__(self, ref: str, context: str | None = None):
192
+ def __init__(
193
+ self,
194
+ ref: str,
195
+ context: str | None = None,
196
+ ref_path: Sequence[str] = (),
197
+ ):
186
198
  from dataface.core.errors.codes_compile import DF_COMPILE_UNRESOLVED_REFERENCE
187
199
 
188
200
  self.ref = ref
201
+ self.ref_path = tuple(ref_path)
189
202
  ctx_suffix = f" in {context}" if context else ""
190
203
  message = DF_COMPILE_UNRESOLVED_REFERENCE.message_template.format(
191
204
  ref=ref, context=ctx_suffix
@@ -51,8 +51,8 @@ class _BaseChartFields(BaseModel):
51
51
  default_factory=set,
52
52
  description="Variable names this chart depends on.",
53
53
  )
54
- source_path: str | None = Field(
55
- default=None,
54
+ source_path: str = Field(
55
+ default="",
56
56
  description="YAML path for edit-back support (e.g. 'charts.revenue').",
57
57
  )
58
58
  query_is_inline: bool = Field(
@@ -31,8 +31,8 @@ class CalloutChart(BaseModel):
31
31
  style: CalloutChartStylePatch | None = Field(
32
32
  default=None, description="Chart-local style overrides."
33
33
  )
34
- source_path: str | None = Field(
35
- default=None,
34
+ source_path: str = Field(
35
+ default="",
36
36
  description="YAML path for edit-back support.",
37
37
  )
38
38
  variable_dependencies: set[str] = Field(
@@ -66,6 +66,10 @@ class LayoutItem(BaseModel):
66
66
  face: "Face | None" = Field(
67
67
  default=None, description="Compiled face, present when type == 'face'."
68
68
  )
69
+ source_path: str = Field(
70
+ default="",
71
+ description="YAML path to this layout item, e.g. rows[0] or cols[1].",
72
+ )
69
73
 
70
74
  # Grid positioning (optional) - 0-based, in grid units
71
75
  row: int | None = Field(
@@ -27,6 +27,7 @@ class ResolvedLayoutItem:
27
27
  y: float
28
28
  width: float
29
29
  height: float
30
+ source_path: str = ""
30
31
  # Carry-through optional fields from LayoutItem
31
32
  details_variable: str | None = None
32
33
  details_summary: str | None = None
@@ -213,13 +213,6 @@ class FontStyle(_PatchBase):
213
213
  "defaults to 1.25; titles override tighter via their font patch in theme YAML."
214
214
  ),
215
215
  )
216
- # None on most font slots; set only on axis label fonts via theme YAML.
217
- # Bounds how far the shrink strategy may reduce the font before falling
218
- # through to tilt or skip.
219
- min_size: float | None = Field(
220
- default=None,
221
- description="Minimum font size in pixels; used by the axis label shrink strategy.",
222
- )
223
216
 
224
217
 
225
218
  _CSS_NAMED_COLORS = frozenset(
@@ -263,9 +256,6 @@ class ResolvedFontStyle(BaseModel):
263
256
  decoration: Literal["none", "line-through", "underline"]
264
257
  case: Literal["none", "sentence", "title", "upper", "lower", "slug", "camel"]
265
258
  line_height: float
266
- # None on most resolved fonts; populated only when the cascaded FontStyle
267
- # carries a min_size (axis label fonts via theme YAML).
268
- min_size: float | None = None
269
259
 
270
260
 
271
261
  class RuleStyle(BaseModel):
@@ -79,10 +79,10 @@ class ResolvedAxisLabelOverlapConfig:
79
79
  """Resolved axis label overlap strategy enablement — all fields required.
80
80
 
81
81
  True = strategy enabled; False = disabled. Fixed application order in the
82
- resolver: shrink → tilt → skip.
82
+ resolver: tilt → skip. Categorical axes only — temporal-bucketed axes
83
+ step the label cadence instead (see emitters/_label_overlap.py).
83
84
  """
84
85
 
85
- shrink: bool
86
86
  tilt: bool
87
87
  skip: bool
88
88
 
@@ -238,6 +238,8 @@ class ResolvedLegendStyle(BaseModel):
238
238
 
239
239
  position: str
240
240
  direction: str
241
+ columns: int
242
+ compact_columns: int
241
243
  label: ResolvedLegendElementStyle
242
244
  title: ResolvedLegendElementStyle
243
245
  interactive_legend: bool
@@ -307,8 +309,7 @@ class ResolvedChartsStyle:
307
309
  area: AreaChartStyle
308
310
  scatter: ScatterChartStyle
309
311
  view: ViewStyle
310
- # Series-label primitive — shared typography for endpoint labels,
311
- # direct stack labels, and slice callouts.
312
+ # Series-label primitive — shared typography for endpoint and stack labels.
312
313
  series_label: SeriesLabelStyle
313
314
  pie: PieChartStyle
314
315
  geoshape: GeoshapeChartStyle
@@ -101,7 +101,6 @@ def resolve_cascaded_font(font: FontStyle, path: str = "") -> ResolvedFontStyle:
101
101
  decoration=font.decoration,
102
102
  case=font.case,
103
103
  line_height=font.line_height,
104
- min_size=font.min_size,
105
104
  )
106
105
 
107
106
 
@@ -469,20 +468,18 @@ def _resolve_overlap_config(
469
468
 
470
469
  Returns None when the cascade did not populate overlap (resolver will skip).
471
470
  Raises if the cascade yielded a partial struct with any None fields — the
472
- theme must supply all three bools via _base.yaml so this never fires in normal
471
+ theme must supply both bools via _base.yaml so this never fires in normal
473
472
  operation; it fires only when overlap is present but incomplete, which signals
474
473
  a broken theme.
475
474
  """
476
475
  if overlap is None:
477
476
  return None
478
- if overlap.shrink is None or overlap.tilt is None or overlap.skip is None:
477
+ if overlap.tilt is None or overlap.skip is None:
479
478
  raise ValueError(
480
479
  f"axis.label.overlap has None fields after cascade: {overlap!r}; "
481
- "theme must supply all three bools (shrink, tilt, skip) via _base.yaml"
480
+ "theme must supply both bools (tilt, skip) via _base.yaml"
482
481
  )
483
- return ResolvedAxisLabelOverlapConfig(
484
- shrink=overlap.shrink, tilt=overlap.tilt, skip=overlap.skip
485
- )
482
+ return ResolvedAxisLabelOverlapConfig(tilt=overlap.tilt, skip=overlap.skip)
486
483
 
487
484
 
488
485
  def build_resolved_axis(axis: AxisStyle) -> ResolvedAxisStyle:
@@ -556,6 +553,8 @@ def _build_resolved_legend(legend: LegendStyle) -> ResolvedLegendStyle:
556
553
  return ResolvedLegendStyle(
557
554
  position=legend.position,
558
555
  direction=legend.direction,
556
+ columns=legend.columns,
557
+ compact_columns=legend.compact_columns,
559
558
  visible=legend.visible,
560
559
  interactive_legend=legend.interactive_legend,
561
560
  symbol_limit=legend.symbol_limit,
@@ -158,6 +158,7 @@ from dataface.core.compile.models.style.theme.marks import (
158
158
  ProjectionStyle,
159
159
  RectMarkStyle,
160
160
  RuleMarkStyle,
161
+ SeriesLabelFontStyle,
161
162
  SeriesLabelStyle,
162
163
  SliceLabelsStyle,
163
164
  SliceMarkStyle,
@@ -321,6 +322,7 @@ __all__ = [
321
322
  "ScatterChartMarksStyle",
322
323
  "ScatterChartStyle",
323
324
  "ScatterLayerStyle",
325
+ "SeriesLabelFontStyle",
324
326
  "SeriesLabelStyle",
325
327
  "SliceLabelsStyle",
326
328
  "SliceMarkStyle",
@@ -16,30 +16,27 @@ from dataface.core.compile.models.markers import (
16
16
 
17
17
 
18
18
  class AxisLabelOverlapConfig(BaseModel):
19
- """Three-bool overlap strategy enablement for axis labels.
19
+ """Two-bool overlap strategy enablement for CATEGORICAL axis labels.
20
20
 
21
21
  Each bool enables (True) or disables (False) a strategy step.
22
22
  None means "not authored — inherit from the theme cascade."
23
- The resolver applies enabled strategies in fixed order: shrink → tilt → skip.
23
+ The resolver applies enabled strategies in fixed order: tilt → skip.
24
+ Temporal-bucketed axes don't use this walk at all — they step the label
25
+ cadence instead (coarsen month → quarter → year at a constant font size
26
+ and angle=0); see resolve_axis_x_overlap in emitters/_label_overlap.py.
24
27
 
25
28
  Attributes:
26
- shrink: Reduce font size to the type-stack floor (min_size) if needed.
27
29
  tilt: Walk tilt_increments and apply the first angle that fits.
28
30
  skip: Emit labelOverlap:parity (every other label dropped).
29
31
 
30
32
  Example YAML:
31
33
  overlap:
32
- shrink: true
33
34
  tilt: true
34
35
  skip: true
35
36
  """
36
37
 
37
38
  model_config = ConfigDict(extra="forbid", frozen=True)
38
39
 
39
- shrink: bool | None = Field(
40
- default=None,
41
- description="Enable font-size shrink strategy; None inherits from parent axis.",
42
- )
43
40
  tilt: bool | None = Field(
44
41
  default=None,
45
42
  description="Enable label tilt strategy; None inherits from parent axis.",
@@ -261,12 +258,13 @@ class AxisElementStyle(BaseModel):
261
258
  default=None,
262
259
  description="Vertical text baseline of labels; None uses Vega-Lite's default.",
263
260
  )
264
- # Three-bool struct enabling/disabling each overlap strategy.
261
+ # Two-bool struct enabling/disabling each overlap strategy (categorical axes
262
+ # only — temporal-bucketed axes step the label cadence instead).
265
263
  # None = not authored — inherit the parent axis's setting from the cascade.
266
264
  # SkipInheritSlots(cascade=True): if overlap is None on the child axis,
267
265
  # the whole struct is copied from the parent; if present, individual None
268
266
  # fields are filled from the parent (enabling partial authoring like
269
- # `overlap: { tilt: false }` while inheriting shrink and skip).
267
+ # `overlap: { tilt: false }` while inheriting skip).
270
268
  overlap: Annotated[
271
269
  AxisLabelOverlapConfig | None,
272
270
  SkipInheritSlots(cascade=True),
@@ -275,7 +273,8 @@ class AxisElementStyle(BaseModel):
275
273
  description=(
276
274
  "Label overlap strategy enablement. None inherits from the theme cascade. "
277
275
  "Set individual bools to enable/disable; the resolver applies them in fixed "
278
- "order shrink→tilt→skip."
276
+ "order tilt→skip. Temporal-bucketed axes ignore this and step the label "
277
+ "cadence instead."
279
278
  ),
280
279
  )
281
280
  separation: float | None = Field(
@@ -892,7 +891,7 @@ class AxisStyle(BaseModel):
892
891
  # Fiscal-year-start month for year/yearquarter bucket anchoring on axis_x.
893
892
  # 1 (January) is the calendar-quarter convention; any other value shifts
894
893
  # both the bucket boundaries (_enumerate_buckets) and the Q1..Q4 numbering
895
- # / year-boundary detection (_is_label_opener, opens_label_period,
894
+ # / year-boundary detection (is_label_opener, opens_label_period,
896
895
  # _quarter_label) by the same offset. Required (like fill) since the model
897
896
  # enforces it on every axis variant; semantically only active on axis_x
898
897
  # for the year/yearquarter grains — yearmonth/yearweek/yearmonthdate
@@ -10,6 +10,7 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_valida
10
10
  from dataface.core.compile.models.markers import (
11
11
  InheritSlot,
12
12
  Merge,
13
+ SkipInheritSlots,
13
14
  Strategy,
14
15
  )
15
16
  from dataface.core.compile.models.primitives import (
@@ -312,6 +313,9 @@ class TitleStyle(BaseModel):
312
313
  font: Annotated[FontStyle, InheritSlot(from_path="Style.font")] = Field(
313
314
  default_factory=FontStyle, description="Title font style overrides."
314
315
  )
316
+ compact_weight: Annotated[str | float, SkipInheritSlots()] = Field(
317
+ description="Object-title font weight on tiny cards."
318
+ )
315
319
  sizes: Annotated[list[float], Merge(Strategy.OVERRIDE)] = Field(
316
320
  description=(
317
321
  "Font sizes for the H1–H6 heading ramp, indexed by ``face.level - 1``. "
@@ -204,10 +204,10 @@ class ChartsStyle(_ChartStyleBase):
204
204
  point_map: PointMapChartStyle = Field(description="Point map chart style.")
205
205
  pie: PieChartStyle = Field(description="Pie/donut chart style.")
206
206
 
207
- # Series-label primitive — typography for series-naming text marks
208
- # (endpoint labels, direct stack labels, slice callouts).
207
+ # Series-label primitive — typography for Cartesian series-naming text marks
208
+ # (endpoint labels and direct stack labels).
209
209
  series_label: SeriesLabelStyle = Field(
210
- description="Shared series-label typography for endpoint labels, stack labels, and slice callouts."
210
+ description="Shared series-label typography for endpoint and stack labels."
211
211
  )
212
212
 
213
213
  # Non-Vega chart types (required — theme pipeline always populates)
@@ -49,6 +49,17 @@ class LegendStyle(BaseModel):
49
49
  direction: str = Field(
50
50
  description="Legend layout direction ('vertical' or 'horizontal')."
51
51
  )
52
+ columns: int = Field(
53
+ ge=0,
54
+ description=(
55
+ "Legend entry columns. Zero keeps the renderer default; positive values "
56
+ "set Vega-Lite legend columns."
57
+ ),
58
+ )
59
+ compact_columns: int = Field(
60
+ ge=1,
61
+ description="Entry columns for an automatic compact top-horizontal legend.",
62
+ )
52
63
  label: LegendLabelStyle = Field(description="Legend label style.")
53
64
  title: LegendTitleStyle = Field(description="Legend title style.")
54
65
  visible: bool | None = Field(