dataface 0.3.2.dev111__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 (73) hide show
  1. dataface/DATAFACE_SYNTAX.md +15 -4
  2. dataface/agent_api/docs/yaml-reference.md +33 -133
  3. dataface/agent_api/serve.py +0 -2
  4. dataface/agent_api/validate.py +8 -0
  5. dataface/cli/commands/render.py +6 -0
  6. dataface/cli/commands/serve.py +0 -6
  7. dataface/cli/main.py +20 -14
  8. dataface/core/aliases.py +27 -0
  9. dataface/core/compile/__init__.py +0 -8
  10. dataface/core/compile/channel.py +0 -13
  11. dataface/core/compile/compiler.py +73 -71
  12. dataface/core/compile/introspection.py +2 -13
  13. dataface/core/compile/models/chart/authored/_base.py +1 -1
  14. dataface/core/compile/models/chart/normalized/__init__.py +2 -0
  15. dataface/core/compile/models/chart/normalized/_base.py +26 -0
  16. dataface/core/compile/models/config.py +5 -0
  17. dataface/core/compile/models/face/authored.py +24 -89
  18. dataface/core/compile/models/query/authored.py +1 -119
  19. dataface/core/compile/models/refs.py +0 -2
  20. dataface/core/compile/models/source.py +73 -159
  21. dataface/core/compile/models/style/theme/variables.py +0 -3
  22. dataface/core/compile/normalize_charts.py +4 -152
  23. dataface/core/compile/normalize_layout.py +20 -284
  24. dataface/core/compile/normalize_queries.py +3 -506
  25. dataface/core/compile/normalizer.py +4 -10
  26. dataface/core/compile/palette.py +19 -7
  27. dataface/core/compile/validator.py +121 -44
  28. dataface/core/compile/yaml_error_formatter.py +107 -2
  29. dataface/core/dashboard.py +3 -1
  30. dataface/core/defaults/default_config.yml +4 -0
  31. dataface/core/defaults/themes/_base.yaml +0 -1
  32. dataface/core/defaults/themes/editorial.yaml +3 -0
  33. dataface/core/defaults/themes/stark.yaml +8 -0
  34. dataface/core/diagnostics/__init__.py +10 -0
  35. dataface/core/diagnostics/codes_compile.py +4 -2
  36. dataface/core/diagnostics/codes_execute.py +70 -0
  37. dataface/core/diagnostics/codes_render.py +58 -4
  38. dataface/core/execute/adapters/__init__.py +1 -1
  39. dataface/core/execute/cache_keys.py +4 -9
  40. dataface/core/execute/executor.py +25 -3
  41. dataface/core/execute/file_source_materializer.py +185 -32
  42. dataface/core/execute/trivial_local_cache.py +5 -2
  43. dataface/core/inspect/__init__.py +0 -1
  44. dataface/core/project.py +6 -2
  45. dataface/core/render/chart/mark_extents.py +339 -0
  46. dataface/core/render/chart/rendering.py +97 -34
  47. dataface/core/render/conditions.py +122 -0
  48. dataface/core/render/controls.py +60 -187
  49. dataface/core/render/data_format.py +1 -1
  50. dataface/core/render/face_resolve.py +7 -0
  51. dataface/core/render/faces.py +94 -30
  52. dataface/core/render/layout_sizing.py +46 -0
  53. dataface/core/render/renderer.py +14 -1
  54. dataface/core/render/sizing.py +42 -105
  55. dataface/core/render/svg_utils.py +60 -14
  56. dataface/core/render/terminal.py +1 -1
  57. dataface/core/render/variables_layout.py +211 -0
  58. dataface/core/render/variables_resolve.py +376 -0
  59. dataface/core/render/variables_strip.py +291 -368
  60. dataface/core/render/warnings/base.py +18 -0
  61. dataface/core/render/warnings/layout_min_exceeds_height.py +68 -0
  62. dataface/core/render/warnings/registry.py +2 -0
  63. dataface/core/serve/alias_index.py +16 -22
  64. dataface/data/schemas/board-resolved/board-resolved.schema.json +1 -499
  65. {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/METADATA +1 -3
  66. {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/RECORD +69 -67
  67. dataface/core/inspect/partials/categorical.yml +0 -38
  68. dataface/core/inspect/partials/date.yml +0 -38
  69. dataface/core/inspect/partials/numeric.yml +0 -53
  70. dataface/core/inspect/templates/charts.yml +0 -94
  71. {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/WHEEL +0 -0
  72. {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/entry_points.txt +0 -0
  73. {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/licenses/LICENSE +0 -0
@@ -32,19 +32,80 @@ See also:
32
32
  - compile/normalizer.py: Next step
33
33
  """
34
34
 
35
+ import re
35
36
  from collections.abc import Mapping
36
37
  from typing import Any
37
38
 
38
39
  from dataface.core.compile.errors import ValidationError
39
40
  from dataface.core.compile.models.chart.authored import (
41
+ AuthoredChart,
40
42
  CalloutChart,
41
43
  _BaseChartFields,
42
44
  )
43
- from dataface.core.compile.models.face.authored import AuthoredFace
45
+ from dataface.core.compile.models.face.authored import (
46
+ AuthoredFace,
47
+ TabItem,
48
+ )
44
49
  from dataface.core.compile.models.query.authored import _BaseQueryFields
45
50
  from dataface.core.compile.models.refs import ChartRef, QueryRef
46
51
  from dataface.core.compile.parser import looks_like_sql
47
- from dataface.core.diagnostics.codes_compile import ERR_UNKNOWN_QUERY
52
+ from dataface.core.diagnostics.codes_compile import (
53
+ ERR_UNKNOWN_QUERY,
54
+ ERR_UNRESOLVED_REFERENCE,
55
+ )
56
+
57
+ LayoutNode = str | AuthoredFace | AuthoredChart | TabItem | dict[str, AuthoredChart]
58
+
59
+
60
+ _DOTTED_INDEX = re.compile(r"\.(\d+)(?=\.|$)")
61
+
62
+
63
+ def _display_location(location: str) -> str:
64
+ """A layout location in the click-to-source spelling, for prose.
65
+
66
+ Two vocabularies exist and neither is going away: the source map keys
67
+ sequences by dotted index (`rows.0`), while `LayoutItem.source_path` and
68
+ Cloud's path parser use brackets (`rows[0]`). Locations are carried in the
69
+ source-map spelling because that is what has to resolve; this converts at
70
+ the one boundary where a human reads the value.
71
+ """
72
+ return _DOTTED_INDEX.sub(r"[\1]", location)
73
+
74
+
75
+ def _layout_children(node: LayoutNode) -> list[LayoutNode]:
76
+ """Direct layout children of a face or tab node."""
77
+ if not isinstance(node, (AuthoredFace, TabItem)):
78
+ return []
79
+ children: list[LayoutNode] = []
80
+ if node.rows:
81
+ children.extend(node.rows)
82
+ if node.cols:
83
+ children.extend(node.cols)
84
+ if node.grid:
85
+ children.extend(gi.item for gi in node.grid.items)
86
+ if node.tabs:
87
+ children.extend(node.tabs.items)
88
+ return children
89
+
90
+
91
+ def _declared_names(face: AuthoredFace) -> tuple[set[str], set[str]]:
92
+ """Chart and query names declared anywhere in the face tree.
93
+
94
+ The compiler's chart and query registries are board-global — any layout
95
+ may reference any chart declared anywhere in the tree — so validation has
96
+ to resolve against that same namespace. Resolving against only the top
97
+ level reports valid cross-face authoring as an unresolved reference.
98
+ """
99
+ charts: set[str] = set()
100
+ queries: set[str] = set()
101
+ stack: list[LayoutNode] = [face]
102
+ while stack:
103
+ node = stack.pop()
104
+ if isinstance(node, AuthoredFace):
105
+ charts |= set(node.charts or {})
106
+ queries |= set(node.queries or {})
107
+ stack.extend(_layout_children(node))
108
+ return charts, queries
48
109
 
49
110
 
50
111
  def validate_face(face: AuthoredFace) -> list[ValidationError]:
@@ -74,9 +135,8 @@ def validate_face(face: AuthoredFace) -> list[ValidationError]:
74
135
  """
75
136
  errors: list[ValidationError] = []
76
137
 
77
- # Collect available names
78
- query_names = set((face.queries or {}).keys())
79
- chart_names = set((face.charts or {}).keys())
138
+ # Board-global, matching the registries the compiler resolves against.
139
+ chart_names, query_names = _declared_names(face)
80
140
 
81
141
  # Validate chart → query references
82
142
  errors.extend(_validate_chart_query_references(face.charts or {}, query_names))
@@ -173,11 +233,16 @@ def _validate_layout_references(
173
233
  continue
174
234
 
175
235
  if item not in chart_names:
176
- errors.append(
177
- ValidationError(
178
- f"Layout references unknown chart '{item}'", location=location
179
- )
236
+ error = ValidationError.from_code(
237
+ ERR_UNRESOLVED_REFERENCE, ref=item, context=" in layout item"
180
238
  )
239
+ # `{context}` is a pre-formatted message suffix, but `fields`
240
+ # is the machine-readable surface — store the bare value, so
241
+ # this agrees with the `ReferenceError` site that raises the
242
+ # same code from the normalizer.
243
+ error.fields["context"] = "layout item"
244
+ error.field_path = location
245
+ errors.append(error)
181
246
 
182
247
  elif isinstance(item, (dict, _BaseChartFields)):
183
248
  # Inline chart - validate its query reference
@@ -199,13 +264,16 @@ def _validate_layout_references(
199
264
  if "." not in query_ref and query_ref not in query_names:
200
265
  if looks_like_sql(query_ref):
201
266
  continue
202
- errors.append(
203
- ValidationError.from_code(
204
- ERR_UNKNOWN_QUERY,
205
- chart_name=location,
206
- query_name=query_ref,
207
- )
267
+ error = ValidationError.from_code(
268
+ ERR_UNKNOWN_QUERY,
269
+ chart_name=_display_location(location),
270
+ query_name=query_ref,
208
271
  )
272
+ # The offending value is the inline chart's `query:`, not the
273
+ # whole item — same precision as the named-chart site, which
274
+ # resolves to `charts.<id>.query`.
275
+ error.field_path = f"{location}.query"
276
+ errors.append(error)
209
277
 
210
278
  return errors
211
279
 
@@ -213,6 +281,12 @@ def _validate_layout_references(
213
281
  def _get_layout_items(face: AuthoredFace) -> list[tuple[str, Any]]:
214
282
  """Extract all layout items with their locations.
215
283
 
284
+ Locations are source-map keys: dotted all the way down, sequence indices
285
+ included (`rows.0`, `tabs.items.1.rows.0`). They are used both to resolve
286
+ a diagnostic's position and as the human-readable location in its message,
287
+ so there is one spelling rather than a display form and a lookup form that
288
+ can drift.
289
+
216
290
  Args:
217
291
  face: AuthoredFace to extract items from
218
292
 
@@ -223,26 +297,34 @@ def _get_layout_items(face: AuthoredFace) -> list[tuple[str, Any]]:
223
297
 
224
298
  if face.rows:
225
299
  for idx, item in enumerate(face.rows):
226
- items.append((f"rows[{idx}]", item))
227
- items.extend(_get_nested_items(item, f"rows[{idx}]"))
300
+ items.append((f"rows.{idx}", item))
301
+ items.extend(_get_nested_items(item, f"rows.{idx}"))
228
302
 
229
303
  if face.cols:
230
304
  for idx, item in enumerate(face.cols):
231
- items.append((f"cols[{idx}]", item))
232
- items.extend(_get_nested_items(item, f"cols[{idx}]"))
305
+ items.append((f"cols.{idx}", item))
306
+ items.extend(_get_nested_items(item, f"cols.{idx}"))
233
307
 
234
308
  if face.grid:
309
+ # `GridItem.item` is a nested field, so the authored scalar lives at
310
+ # `grid.items.<n>.item` — `grid.items.<n>` is the wrapper block.
235
311
  for idx, grid_item in enumerate(face.grid.items):
236
- items.append((f"grid.items[{idx}]", grid_item.item))
312
+ location = f"grid.items.{idx}.item"
313
+ items.append((location, grid_item.item))
314
+ items.extend(_get_nested_items(grid_item.item, location))
237
315
 
238
316
  if face.tabs:
239
317
  for idx, tab_item in enumerate(face.tabs.items):
240
318
  if tab_item.rows:
241
319
  for i, item in enumerate(tab_item.rows):
242
- items.append((f"tabs.items[{idx}].rows[{i}]", item))
320
+ location = f"tabs.items.{idx}.rows.{i}"
321
+ items.append((location, item))
322
+ items.extend(_get_nested_items(item, location))
243
323
  if tab_item.cols:
244
324
  for i, item in enumerate(tab_item.cols):
245
- items.append((f"tabs.items[{idx}].cols[{i}]", item))
325
+ location = f"tabs.items.{idx}.cols.{i}"
326
+ items.append((location, item))
327
+ items.extend(_get_nested_items(item, location))
246
328
 
247
329
  return items
248
330
 
@@ -250,6 +332,13 @@ def _get_layout_items(face: AuthoredFace) -> list[tuple[str, Any]]:
250
332
  def _get_nested_items(item: Any, parent_location: str) -> list[tuple[str, Any]]:
251
333
  """Extract items from nested face structures.
252
334
 
335
+ A nested face reaches here as an `AuthoredFace`: the parser types layout
336
+ items, so matching `dict` would miss every nesting.
337
+
338
+ A nested face declaring its own `charts:` needs no special case — those
339
+ names are already in the board-global set `_declared_names` collects, so
340
+ its items resolve like any other.
341
+
253
342
  Args:
254
343
  item: Item that might be a nested face
255
344
  parent_location: Parent location string
@@ -257,29 +346,17 @@ def _get_nested_items(item: Any, parent_location: str) -> list[tuple[str, Any]]:
257
346
  Returns:
258
347
  List of (location, item) tuples from nested structures
259
348
  """
260
- items: list[tuple[str, Any]] = []
261
-
262
- if isinstance(item, dict):
263
- # If nested face has its own charts, skip validation here -
264
- # the nested face's layout items will reference its own charts,
265
- # which will be validated during normalization
266
- if "charts" in item:
267
- return items
268
-
269
- # Nested face without its own charts - extract its layout items
270
- if "rows" in item:
271
- for idx, nested in enumerate(item["rows"]):
272
- items.append((f"{parent_location}.rows[{idx}]", nested))
273
- items.extend(
274
- _get_nested_items(nested, f"{parent_location}.rows[{idx}]")
275
- )
276
- if "cols" in item:
277
- for idx, nested in enumerate(item["cols"]):
278
- items.append((f"{parent_location}.cols[{idx}]", nested))
279
- items.extend(
280
- _get_nested_items(nested, f"{parent_location}.cols[{idx}]")
281
- )
349
+ if not isinstance(item, AuthoredFace):
350
+ return []
282
351
 
352
+ items: list[tuple[str, Any]] = []
353
+ for key, entries in (("rows", item.rows), ("cols", item.cols)):
354
+ if entries is None:
355
+ continue
356
+ for idx, entry in enumerate(entries):
357
+ location = f"{parent_location}.{key}.{idx}"
358
+ items.append((location, entry))
359
+ items.extend(_get_nested_items(entry, location))
283
360
  return items
284
361
 
285
362
 
@@ -10,6 +10,7 @@ Refs #94
10
10
  """
11
11
 
12
12
  import difflib
13
+ import re
13
14
  from collections.abc import Sequence
14
15
  from dataclasses import dataclass
15
16
  from functools import lru_cache
@@ -62,7 +63,6 @@ _PRIMITIVE_TYPE_DISCRIMINATORS = {"str", "int", "float", "bool", "bytes", "None"
62
63
 
63
64
  # Names of our known union-branch model types (not real YAML fields)
64
65
  _MODEL_TYPE_DISCRIMINATORS = {
65
- "ForeachItem",
66
66
  "ChartPatch",
67
67
  "AuthoredFace",
68
68
  "Variable",
@@ -116,7 +116,7 @@ def _collapse_union_validation_errors(
116
116
  """Collapse Pydantic union-branch noise into the most informative errors.
117
117
 
118
118
  Pydantic tries every branch of a union type annotation (str | AuthoredFace
119
- | ChartPatch | ForeachItem | dict[str,ChartPatch]) for each
119
+ | ChartPatch | dict[str,ChartPatch]) for each
120
120
  layout row. A single invalid row can produce 14+ error dicts — one per
121
121
  (branch × wrong-field) combination. This function keeps only the errors
122
122
  from the best-matching branch per unique loc prefix.
@@ -716,6 +716,95 @@ def format_validation_errors_structured(
716
716
  return structured
717
717
 
718
718
 
719
+ _SHAPE_NOUN_SEPARATORS = re.compile(r"[\s_-]+")
720
+
721
+
722
+ def _normalize_shape_noun(value: str) -> str:
723
+ """Fold the spellings of one shape noun together.
724
+
725
+ Authors reach for `streamgraph`, `stream_graph`, and `Stream Graph` for the same
726
+ chart, so separators are dropped rather than unified — otherwise every entry below
727
+ would need a row per punctuation variant.
728
+ """
729
+ return _SHAPE_NOUN_SEPARATORS.sub("", value.strip().lower())
730
+
731
+
732
+ # Chart shapes the world names that Dataface draws by composing existing fields
733
+ # rather than by a `type:` tag. The value spells the recipe out: an author who
734
+ # reached this error already looked for the noun and did not find it, so a bare
735
+ # pointer to the docs repeats the failure. Every entry is pinned by a spec
736
+ # assertion in tests/core/test_chart_shape_recipes.py.
737
+ _CHART_SHAPE_RECIPES = {
738
+ "streamgraph": "type: area with color: and style.stack: center",
739
+ "stacked_area": "type: area with color: and style.stack: zero",
740
+ "stacked_bar": "type: bar with color: and style.stack: zero",
741
+ "grouped_bar": "type: bar with color: and style.stack: none",
742
+ "clustered_bar": "type: bar with color: and style.stack: none",
743
+ "horizontal_bar": "type: bar with style.orientation: horizontal",
744
+ "column": "type: bar with style.orientation: vertical",
745
+ "100% stacked": "type: bar or type: area with color: and style.stack: normalize",
746
+ "percent_stacked_bar": "type: bar with color: and style.stack: normalize",
747
+ "normalized_bar": "type: bar with color: and style.stack: normalize",
748
+ "small_multiples": "multiples.rows: <column> (or multiples.columns:) on a cartesian chart",
749
+ "trellis": "multiples.rows: <column> (or multiples.columns:) on a cartesian chart",
750
+ "faceted": "multiples.rows: <column> (or multiples.columns:) on a cartesian chart",
751
+ "dual_axis": (
752
+ "layers: on a cartesian chart, with axis_y.position: right on the "
753
+ "added layer for its own y-axis"
754
+ ),
755
+ "combo": (
756
+ "layers: on a cartesian chart, with axis_y.position: right on the "
757
+ "added layer for its own y-axis"
758
+ ),
759
+ "bar_and_line": (
760
+ "layers: on a cartesian chart, with axis_y.position: right on the "
761
+ "added layer for its own y-axis"
762
+ ),
763
+ # The _column nouns promise a vertical layout, which orientation inference
764
+ # doesn't guarantee for a categorical x — style.orientation: vertical is
765
+ # explicit here, unlike the plain _bar recipes above.
766
+ "stacked_column": "type: bar with style.orientation: vertical, color:, and style.stack: zero",
767
+ "grouped_column": "type: bar with style.orientation: vertical, color:, and style.stack: none",
768
+ "clustered_column": "type: bar with style.orientation: vertical, color:, and style.stack: none",
769
+ "row_chart": "type: bar with style.orientation: horizontal",
770
+ "vertical_bar": "type: bar with style.orientation: vertical",
771
+ "stream_chart": "type: area with color: and style.stack: center",
772
+ }
773
+
774
+ # Shapes Dataface cannot draw at all. Naming them is the half a recipe cannot
775
+ # cover: without it an author settles for the nearest tag, which validates clean
776
+ # and renders the wrong chart.
777
+ _UNSUPPORTED_CHART_SHAPES = frozenset(
778
+ {
779
+ "sankey",
780
+ "treemap",
781
+ "violin",
782
+ "waterfall",
783
+ "funnel",
784
+ "gauge",
785
+ "radar",
786
+ "sunburst",
787
+ "chord",
788
+ "marimekko",
789
+ "spider",
790
+ "alluvial",
791
+ "gantt",
792
+ "word cloud",
793
+ "network",
794
+ "candlestick",
795
+ }
796
+ )
797
+
798
+ # Both tables are keyed for reading; lookups go through the normalized forms so a
799
+ # noun is spelled once at the source of truth.
800
+ _RECIPE_BY_NORMALIZED_NOUN = {
801
+ _normalize_shape_noun(noun): recipe for noun, recipe in _CHART_SHAPE_RECIPES.items()
802
+ }
803
+ _NORMALIZED_UNSUPPORTED_SHAPES = frozenset(
804
+ _normalize_shape_noun(noun) for noun in _UNSUPPORTED_CHART_SHAPES
805
+ )
806
+
807
+
719
808
  def _format_chart_type_discriminator_error(
720
809
  err: dict[str, Any],
721
810
  field_path: list[str],
@@ -735,6 +824,22 @@ def _format_chart_type_discriminator_error(
735
824
  if not isinstance(invalid_type, str):
736
825
  return f"Missing required field 'type'. Supported chart types: {supported}."
737
826
 
827
+ # A known shape noun beats a fuzzy tag guess: both branches below answer the
828
+ # question the author actually asked, so neither falls through to "did you mean".
829
+ normalized = _normalize_shape_noun(invalid_type)
830
+ recipe = _RECIPE_BY_NORMALIZED_NOUN.get(normalized)
831
+ if recipe:
832
+ return (
833
+ f"Unknown chart type {invalid_type!r}. Dataface draws that shape as "
834
+ f"{recipe}. Run `dft docs charts` for the chart reference. "
835
+ f"Supported chart types: {supported}."
836
+ )
837
+ if normalized in _NORMALIZED_UNSUPPORTED_SHAPES:
838
+ return (
839
+ f"Unknown chart type {invalid_type!r}. Dataface cannot draw that shape "
840
+ f"today. Supported chart types: {supported}."
841
+ )
842
+
738
843
  suggestion = suggest_similar_value(invalid_type, valid_chart_types)
739
844
  if suggestion:
740
845
  return (
@@ -27,7 +27,7 @@ from typing import Any, Literal, get_args
27
27
 
28
28
  _logger = logging.getLogger(__name__)
29
29
 
30
- from pydantic import BaseModel
30
+ from pydantic import BaseModel, ConfigDict
31
31
 
32
32
  from dataface.core.compile import (
33
33
  CompileResult,
@@ -94,6 +94,8 @@ RenderStatus = Literal["ok", "partial", "failed"]
94
94
 
95
95
 
96
96
  class RenderedDashboard(BaseModel):
97
+ model_config = ConfigDict(extra="forbid")
98
+
97
99
  status: RenderStatus
98
100
  # bytes for binary formats (png/pdf); str for text-based; dict for json.
99
101
  # MCP callers should not request binary formats — `model_dump_json()` /
@@ -76,6 +76,10 @@ cache:
76
76
  execution:
77
77
  max_workers: 8
78
78
  max_query_duration_seconds: 120
79
+ # Hard cap on matched files per glob pattern in file sources. Prevents
80
+ # runaway fan-out when a wildcard accidentally matches thousands of files.
81
+ # Override in dataface.yml under execution.max_glob_file_count.
82
+ max_glob_file_count: 1000
79
83
  dialect_aliases:
80
84
  sqlserver: tsql
81
85
  mssql: tsql
@@ -939,7 +939,6 @@ style:
939
939
  title_inline_band_bottom_pad: 24.0
940
940
  gap: 10.0
941
941
  label_position: inline
942
- line_height: 24.0
943
942
  padding: 4.0
944
943
  container_height: 36.0
945
944
  container_padding: 8.0
@@ -202,6 +202,9 @@ style:
202
202
  header:
203
203
  font:
204
204
  color: dft-grays.heading
205
+ row:
206
+ stripe:
207
+ color: dft-grays.surface-subtle # subtle zebra (one scaffold stop off canvas)
205
208
  variables:
206
209
  # Variable-strip labels track the muted gray rather than the
207
210
  # cooler #666666 stark uses.
@@ -63,6 +63,7 @@ style:
63
63
  color: *color_ink
64
64
 
65
65
  legend:
66
+ visible: false # direct-labeling by default (editorial posture)
66
67
  label:
67
68
  font:
68
69
  color: *color_ink
@@ -178,6 +179,13 @@ style:
178
179
  bar:
179
180
  endpoint_labels:
180
181
  visible: true
182
+ legend:
183
+ # A bar that direct-labels has this suppressed again by resolve. It is
184
+ # load-bearing for the bars that cannot — grouped, or stacked but
185
+ # disqualified by `multiples:`, a negative measure, or a series absent
186
+ # from the last column — which would otherwise carry no series key at
187
+ # all, since the theme hides legends globally.
188
+ visible: true
181
189
 
182
190
  pie:
183
191
  total:
@@ -48,6 +48,9 @@ from dataface.core.diagnostics.codes_compile import (
48
48
  from dataface.core.diagnostics.codes_execute import (
49
49
  ERR_BINDER_TYPE_MISMATCH,
50
50
  ERR_BINDER_UNKNOWN_COLUMN,
51
+ ERR_GLOB_EMPTY,
52
+ ERR_GLOB_SCHEMA_MISMATCH,
53
+ ERR_GLOB_TOO_MANY,
51
54
  ERR_MUTATING_SQL,
52
55
  ERR_NO_DEFAULT_SOURCE,
53
56
  ERR_QUERY_DURATION_EXCEEDED,
@@ -68,6 +71,7 @@ from dataface.core.diagnostics.codes_query import (
68
71
  )
69
72
  from dataface.core.diagnostics.codes_render import (
70
73
  ERR_BAR_DUPLICATE_ROWS,
74
+ ERR_CHART_PAINTED_NO_MARKS,
71
75
  ERR_CONCAT_OVERSHOOT_NONPOSITIVE,
72
76
  ERR_DUPLICATE_CHART_ID,
73
77
  ERR_EMITTER_NOT_FOUND,
@@ -88,6 +92,7 @@ from dataface.core.diagnostics.codes_render import (
88
92
  ERR_VEGA_LITE_UNSUPPORTED_TYPE,
89
93
  WARN_BAR_BAND_WIDTH_TOO_NARROW,
90
94
  WARN_LAYERED_CHART_SHARED_Y_AXIS_SCALE_MISMATCH,
95
+ WARN_LAYOUT_MIN_EXCEEDS_HEIGHT,
91
96
  WARN_LIKELY_CURRENCY_OR_PERCENT_MISSING_FORMATTER,
92
97
  WARN_PALETTE_UNSUPPORTED,
93
98
  WARN_PIE_DOMINANT_SEGMENT,
@@ -177,6 +182,9 @@ __all__ = [
177
182
  "WARN_H1_BODY_NO_TITLE",
178
183
  "WARN_SINGLE_CHART_REDUNDANT_TITLE",
179
184
  # Execute errors
185
+ "ERR_GLOB_EMPTY",
186
+ "ERR_GLOB_SCHEMA_MISMATCH",
187
+ "ERR_GLOB_TOO_MANY",
180
188
  "ERR_REPO_FILE_TOO_LARGE",
181
189
  "ERR_SOURCE_NOT_FOUND_EMPTY",
182
190
  "ERR_NO_DEFAULT_SOURCE",
@@ -197,6 +205,7 @@ __all__ = [
197
205
  # Render errors
198
206
  "ERR_KPI_MULTIROW",
199
207
  "ERR_BAR_DUPLICATE_ROWS",
208
+ "ERR_CHART_PAINTED_NO_MARKS",
200
209
  "ERR_MAP_LOOKUP_KEY_MISMATCH",
201
210
  "ERR_DUPLICATE_CHART_ID",
202
211
  "ERR_FORMAT_UNSUPPORTED",
@@ -218,6 +227,7 @@ __all__ = [
218
227
  # Render warnings
219
228
  "WARN_BAR_BAND_WIDTH_TOO_NARROW",
220
229
  "WARN_LAYERED_CHART_SHARED_Y_AXIS_SCALE_MISMATCH",
230
+ "WARN_LAYOUT_MIN_EXCEEDS_HEIGHT",
221
231
  "WARN_LIKELY_CURRENCY_OR_PERCENT_MISSING_FORMATTER",
222
232
  "WARN_PALETTE_UNSUPPORTED",
223
233
  "WARN_PIE_DOMINANT_SEGMENT",
@@ -359,12 +359,14 @@ ERR_BAR_LOG_SCALE_NOT_SUPPORTED = REGISTRY.register(
359
359
  message_template=(
360
360
  "Chart {chart_id!r} (bar): axis_y.scale.type: log is not "
361
361
  "supported — a bar's length encodes magnitude from zero, which a "
362
- "log scale makes meaningless. Use line or area, or drop the log scale."
362
+ "log scale makes meaningless. Use scale.type: symlog on the bar "
363
+ "chart, or switch to a line or area chart for a log scale."
363
364
  ),
364
365
  doc=(
365
366
  "Fired when a bar chart's y axis is set to `scale.type: log`. A bar's "
366
367
  "length encodes magnitude from zero, which a log scale makes meaningless. "
367
- "Use a line or area chart, or drop the log scale."
368
+ "Use `scale.type: symlog` on the bar chart, or switch to a line or area "
369
+ "chart for a log scale."
368
370
  ),
369
371
  summary="Fired when a bar chart's y-axis uses a log scale, which magnitude-from-zero bars can't represent.",
370
372
  docs_topic="charts",
@@ -11,6 +11,76 @@ from dataface.core.diagnostics.hints import (
11
11
  )
12
12
  from dataface.core.diagnostics.registry import REGISTRY, ErrorCode
13
13
 
14
+ ERR_GLOB_EMPTY = REGISTRY.register(
15
+ ErrorCode(
16
+ code="ERR-GLOB-EMPTY",
17
+ domain="execute",
18
+ title="Glob pattern in file source matched no files",
19
+ message_template=(
20
+ "File source {source_name!r}, table {table_name!r}: "
21
+ "glob pattern {pattern!r} matched no files. "
22
+ "Provide at least one matching file or fix the pattern."
23
+ ),
24
+ doc=(
25
+ "Fired when a glob pattern in a file source's `files:` mapping expands "
26
+ "to zero files. Every glob must match at least one file — an empty match "
27
+ "is always a configuration error. Check the pattern for typos, verify the "
28
+ "files exist at the expected paths, and confirm the path is relative to "
29
+ "the project root."
30
+ ),
31
+ docs_topic="queries",
32
+ )
33
+ )
34
+
35
+ ERR_GLOB_TOO_MANY = REGISTRY.register(
36
+ ErrorCode(
37
+ code="ERR-GLOB-TOO-MANY",
38
+ domain="execute",
39
+ title="Glob pattern in file source exceeded the file-count cap",
40
+ message_template=(
41
+ "File source {source_name!r}, table {table_name!r}: "
42
+ "glob pattern {pattern!r} matched {count} files, "
43
+ "exceeding the {cap}-file cap. "
44
+ "Increase execution.max_glob_file_count in dataface.yml if needed."
45
+ ),
46
+ summary=(
47
+ "Fired when a glob pattern in a file source matches more files "
48
+ "than the configured cap."
49
+ ),
50
+ doc=(
51
+ "Fired when a glob pattern in a file source's `files:` mapping matches "
52
+ "more files than the configured `execution.max_glob_file_count` limit. "
53
+ "Narrow the pattern, or raise the cap in `dataface.yml` under "
54
+ "`execution: max_glob_file_count: <N>`."
55
+ ),
56
+ docs_topic="queries",
57
+ )
58
+ )
59
+
60
+ ERR_GLOB_SCHEMA_MISMATCH = REGISTRY.register(
61
+ ErrorCode(
62
+ code="ERR-GLOB-SCHEMA-MISMATCH",
63
+ domain="execute",
64
+ title="Glob-matched files have inconsistent column schemas",
65
+ message_template=(
66
+ "File source {source_name!r}, table {table_name!r}: "
67
+ "{path!r} has different columns than {first_path!r}. "
68
+ "{detail}"
69
+ "All files matched by a glob must share the same column schema."
70
+ ),
71
+ summary=(
72
+ "Fired when files matched by a glob pattern have different column sets."
73
+ ),
74
+ doc=(
75
+ "Fired when a glob pattern in a file source's `files:` mapping expands "
76
+ "to files with different column schemas. All matched files must share the "
77
+ "same column names. Align the column schemas across all files, or split "
78
+ "the source into separate entries with non-overlapping patterns."
79
+ ),
80
+ docs_topic="queries",
81
+ )
82
+ )
83
+
14
84
  ERR_REPO_FILE_TOO_LARGE = REGISTRY.register(
15
85
  ErrorCode(
16
86
  code="ERR-REPO-FILE-TOO-LARGE",