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.
- dataface/DATAFACE_SYNTAX.md +15 -4
- dataface/agent_api/docs/yaml-reference.md +33 -133
- dataface/agent_api/serve.py +0 -2
- dataface/agent_api/validate.py +8 -0
- dataface/cli/commands/render.py +6 -0
- dataface/cli/commands/serve.py +0 -6
- dataface/cli/main.py +20 -14
- dataface/core/aliases.py +27 -0
- dataface/core/compile/__init__.py +0 -8
- dataface/core/compile/channel.py +0 -13
- dataface/core/compile/compiler.py +73 -71
- dataface/core/compile/introspection.py +2 -13
- dataface/core/compile/models/chart/authored/_base.py +1 -1
- dataface/core/compile/models/chart/normalized/__init__.py +2 -0
- dataface/core/compile/models/chart/normalized/_base.py +26 -0
- dataface/core/compile/models/config.py +5 -0
- dataface/core/compile/models/face/authored.py +24 -89
- dataface/core/compile/models/query/authored.py +1 -119
- dataface/core/compile/models/refs.py +0 -2
- dataface/core/compile/models/source.py +73 -159
- dataface/core/compile/models/style/theme/variables.py +0 -3
- dataface/core/compile/normalize_charts.py +4 -152
- dataface/core/compile/normalize_layout.py +20 -284
- dataface/core/compile/normalize_queries.py +3 -506
- dataface/core/compile/normalizer.py +4 -10
- dataface/core/compile/palette.py +19 -7
- dataface/core/compile/validator.py +121 -44
- dataface/core/compile/yaml_error_formatter.py +107 -2
- dataface/core/dashboard.py +3 -1
- dataface/core/defaults/default_config.yml +4 -0
- dataface/core/defaults/themes/_base.yaml +0 -1
- dataface/core/defaults/themes/editorial.yaml +3 -0
- dataface/core/defaults/themes/stark.yaml +8 -0
- dataface/core/diagnostics/__init__.py +10 -0
- dataface/core/diagnostics/codes_compile.py +4 -2
- dataface/core/diagnostics/codes_execute.py +70 -0
- dataface/core/diagnostics/codes_render.py +58 -4
- dataface/core/execute/adapters/__init__.py +1 -1
- dataface/core/execute/cache_keys.py +4 -9
- dataface/core/execute/executor.py +25 -3
- dataface/core/execute/file_source_materializer.py +185 -32
- dataface/core/execute/trivial_local_cache.py +5 -2
- dataface/core/inspect/__init__.py +0 -1
- dataface/core/project.py +6 -2
- dataface/core/render/chart/mark_extents.py +339 -0
- dataface/core/render/chart/rendering.py +97 -34
- dataface/core/render/conditions.py +122 -0
- dataface/core/render/controls.py +60 -187
- dataface/core/render/data_format.py +1 -1
- dataface/core/render/face_resolve.py +7 -0
- dataface/core/render/faces.py +94 -30
- dataface/core/render/layout_sizing.py +46 -0
- dataface/core/render/renderer.py +14 -1
- dataface/core/render/sizing.py +42 -105
- dataface/core/render/svg_utils.py +60 -14
- dataface/core/render/terminal.py +1 -1
- dataface/core/render/variables_layout.py +211 -0
- dataface/core/render/variables_resolve.py +376 -0
- dataface/core/render/variables_strip.py +291 -368
- dataface/core/render/warnings/base.py +18 -0
- dataface/core/render/warnings/layout_min_exceeds_height.py +68 -0
- dataface/core/render/warnings/registry.py +2 -0
- dataface/core/serve/alias_index.py +16 -22
- dataface/data/schemas/board-resolved/board-resolved.schema.json +1 -499
- {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/METADATA +1 -3
- {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/RECORD +69 -67
- dataface/core/inspect/partials/categorical.yml +0 -38
- dataface/core/inspect/partials/date.yml +0 -38
- dataface/core/inspect/partials/numeric.yml +0 -53
- dataface/core/inspect/templates/charts.yml +0 -94
- {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/WHEEL +0 -0
- {dataface-0.3.2.dev111.dist-info → dataface-0.3.2.dev150.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
|
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
|
-
#
|
|
78
|
-
query_names =
|
|
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
|
-
|
|
177
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
227
|
-
items.extend(_get_nested_items(item, f"rows
|
|
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
|
|
232
|
-
items.extend(_get_nested_items(item, f"cols
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 |
|
|
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 (
|
dataface/core/dashboard.py
CHANGED
|
@@ -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
|
|
@@ -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
|
|
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
|
|
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",
|