dataface 0.4.1.dev53__py3-none-any.whl → 0.4.1.dev134__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 +4 -2
- dataface/agent_api/board_artifact.py +15 -1
- dataface/agent_api/docs/yaml-reference.md +14 -4
- dataface/agent_api/project_session.py +4 -1
- dataface/agent_api/query.py +53 -7
- dataface/ai/skills/dashboard-build/SKILL.md +2 -0
- dataface/ai/tools/__init__.py +10 -3
- dataface/cli/commands/query.py +1 -1
- dataface/core/compile/data_table.py +8 -8
- dataface/core/compile/format.py +12 -2
- dataface/core/compile/inherit_graph.py +22 -1
- dataface/core/compile/inherit_resolver.py +1 -1
- dataface/core/compile/introspection.py +6 -0
- dataface/core/compile/models/chart/normalized/callout.py +7 -0
- dataface/core/compile/models/chart/resolved/_base.py +13 -0
- dataface/core/compile/models/config.py +0 -17
- dataface/core/compile/models/face/authored.py +12 -7
- dataface/core/compile/models/face/normalized.py +14 -7
- dataface/core/compile/models/face/resolved.py +67 -1
- dataface/core/compile/models/markers.py +20 -0
- dataface/core/compile/models/primitives.py +2 -1
- dataface/core/compile/models/style/authored/table.py +22 -3
- dataface/core/compile/models/style/context.py +225 -0
- dataface/core/compile/models/style/resolved/__init__.py +19 -8
- dataface/core/compile/models/style/resolved/_base.py +63 -124
- dataface/core/compile/models/style/resolved/_cascade.py +94 -51
- dataface/core/compile/models/style/resolved/point_map.py +3 -0
- dataface/core/compile/models/style/resolved/table.py +1 -1
- dataface/core/compile/models/style/theme/__init__.py +2 -4
- dataface/core/compile/models/style/theme/board.py +13 -40
- dataface/core/compile/models/style/theme/charts.py +1 -1
- dataface/core/compile/models/style/theme/marks.py +20 -7
- dataface/core/compile/normalize_layout.py +6 -0
- dataface/core/compile/normalizer.py +48 -19
- dataface/core/compile/parameterized.py +16 -2
- dataface/core/compile/resolve/__init__.py +6 -4
- dataface/core/compile/resolve/_axes.py +28 -19
- dataface/core/compile/resolve/_dispatch.py +18 -20
- dataface/core/compile/resolve/_kwargs.py +90 -46
- dataface/core/compile/resolve/_layers.py +36 -12
- dataface/core/compile/resolve/_marks.py +21 -5
- dataface/core/compile/resolve/_palette.py +25 -18
- dataface/core/compile/resolve/_table.py +42 -25
- dataface/core/compile/resolve/area.py +28 -21
- dataface/core/compile/resolve/bar.py +69 -47
- dataface/core/compile/resolve/geo.py +40 -28
- dataface/core/compile/resolve/heatmap.py +23 -19
- dataface/core/compile/resolve/line.py +31 -22
- dataface/core/compile/resolve/pie.py +25 -18
- dataface/core/compile/resolve/scatter.py +38 -25
- dataface/core/compile/resolve/simple.py +23 -20
- dataface/core/compile/schema_renderers/json_schema.py +5 -1
- dataface/core/compile/schema_renderers/prompt.py +7 -1
- dataface/core/compile/single_series_allocation.py +1 -1
- dataface/core/compile/sizing.py +8 -2
- dataface/core/compile/style_cascade.py +88 -54
- dataface/core/compile/typography.py +18 -18
- dataface/core/compile/validate_formats.py +2 -2
- dataface/core/compile/vega_config.py +3 -3
- dataface/core/compile/vega_lite/mapping.py +23 -23
- dataface/core/dashboard.py +6 -5
- dataface/core/defaults/default_config.yml +0 -39
- dataface/core/defaults/themes/_base.yaml +3 -0
- dataface/core/defaults/themes/solid.yaml +245 -0
- dataface/core/defaults/themes/stark.yaml +1 -1
- dataface/core/diagnostics/codes_render.py +108 -1
- dataface/core/dialects/athena.py +0 -2
- dataface/core/dialects/base.py +0 -40
- dataface/core/dialects/bigquery.py +0 -2
- dataface/core/dialects/databricks.py +0 -2
- dataface/core/dialects/duckdb.py +0 -2
- dataface/core/dialects/mysql.py +0 -2
- dataface/core/dialects/postgres.py +0 -2
- dataface/core/dialects/snowflake.py +0 -2
- dataface/core/dialects/sqlite.py +0 -2
- dataface/core/dialects/sqlserver.py +0 -2
- dataface/core/execute/adapters/adapter_registry.py +31 -0
- dataface/core/execute/chart_resolution.py +3 -3
- dataface/core/fonts.py +5 -5
- dataface/core/render/board_links.py +1 -1
- dataface/core/render/chart/data_table_attachment.py +83 -53
- dataface/core/render/chart/emitters/_cartesian.py +87 -12
- dataface/core/render/chart/emitters/_layers.py +90 -43
- dataface/core/render/chart/emitters/_overlay.py +9 -3
- dataface/core/render/chart/emitters/area.py +41 -16
- dataface/core/render/chart/emitters/bar.py +50 -34
- dataface/core/render/chart/emitters/geo.py +2 -0
- dataface/core/render/chart/emitters/line.py +17 -13
- dataface/core/render/chart/emitters/pie.py +8 -6
- dataface/core/render/chart/emitters/scatter.py +11 -7
- dataface/core/render/chart/features/endpoint_labels.py +3 -14
- dataface/core/render/chart/features/facet.py +12 -10
- dataface/core/render/chart/features/mirror_axis.py +20 -6
- dataface/core/render/chart/kpi.py +2 -2
- dataface/core/render/chart/rendering.py +96 -30
- dataface/core/render/chart/spark.py +7 -7
- dataface/core/render/chart/spec.py +14 -2
- dataface/core/render/chart/spec_builders.py +1 -1
- dataface/core/render/chart/table.py +106 -27
- dataface/core/render/chart/table_support.py +3 -41
- dataface/core/render/chart/time_unit_detect.py +16 -5
- dataface/core/render/chart/type_inference.py +14 -0
- dataface/core/render/chart/vega_lite.py +64 -19
- dataface/core/render/chart/vl_field_maps.py +5 -0
- dataface/core/render/chart_diagnostics.py +27 -0
- dataface/core/render/chart_interactivity.py +4 -2
- dataface/core/render/chrome_css.py +2 -2
- dataface/core/render/column_packer.py +65 -29
- dataface/core/render/converters/chart.py +11 -7
- dataface/core/render/face_resolve.py +57 -23
- dataface/core/render/face_to_dict.py +8 -9
- dataface/core/render/faces.py +4 -4
- dataface/core/render/font_support.py +0 -70
- dataface/core/render/format_utils.py +44 -19
- dataface/core/render/layout_sizing.py +261 -164
- dataface/core/render/layouts.py +6 -6
- dataface/core/render/numeral_expr.py +27 -17
- dataface/core/render/prose.py +29 -25
- dataface/core/render/renderer.py +3 -0
- dataface/core/render/sizing.py +21 -21
- dataface/core/render/templates/scripts/chart_interactivity.js +1 -1
- dataface/core/render/terminal.py +8 -2
- dataface/core/render/terminal_charts.py +2 -1
- dataface/core/render/warnings/value_labels_crowd_width.py +1 -1
- dataface/core/text/format_d3.py +115 -9
- dataface/core/text/numeral_scale.py +8 -7
- dataface/core/utils.py +84 -2
- dataface/data/highlighting/face.json +1 -1
- dataface/data/schemas/board-resolved/board-resolved.schema.json +898 -2880
- {dataface-0.4.1.dev53.dist-info → dataface-0.4.1.dev134.dist-info}/METADATA +3 -3
- {dataface-0.4.1.dev53.dist-info → dataface-0.4.1.dev134.dist-info}/RECORD +136 -133
- {dataface-0.4.1.dev53.dist-info → dataface-0.4.1.dev134.dist-info}/WHEEL +1 -1
- mdsvg/parser.py +17 -0
- mdsvg/renderer.py +175 -12
- {dataface-0.4.1.dev53.dist-info → dataface-0.4.1.dev134.dist-info}/entry_points.txt +0 -0
- {dataface-0.4.1.dev53.dist-info → dataface-0.4.1.dev134.dist-info}/licenses/LICENSE +0 -0
dataface/DATAFACE_SYNTAX.md
CHANGED
|
@@ -307,9 +307,11 @@ style:
|
|
|
307
307
|
text:
|
|
308
308
|
align: left
|
|
309
309
|
column:
|
|
310
|
-
|
|
310
|
+
max_number: 2
|
|
311
311
|
gap: 24
|
|
312
|
-
rule:
|
|
312
|
+
rule:
|
|
313
|
+
width: 1
|
|
314
|
+
color: "#cbd5e1"
|
|
313
315
|
```
|
|
314
316
|
|
|
315
317
|
CSS-only keys like `border-radius`, `border-left`, `margin-top` are not board-style fields and are rejected.
|
|
@@ -22,6 +22,7 @@ from pydantic import BaseModel, ConfigDict
|
|
|
22
22
|
|
|
23
23
|
from dataface.agent_api._paths import resolve_face_or_error
|
|
24
24
|
from dataface.core.compile import compile_file
|
|
25
|
+
from dataface.core.compile.models.face.resolved import ChartResolveFailure
|
|
25
26
|
from dataface.core.compile.variables import normalize_multiselect_values
|
|
26
27
|
from dataface.core.diagnostics import ERR_FILE_NOT_FOUND, Diagnostic
|
|
27
28
|
from dataface.core.diagnostics.base import DatafaceError
|
|
@@ -133,14 +134,27 @@ def emit_board_artifact(
|
|
|
133
134
|
set_filter_variables_context(
|
|
134
135
|
frozenset(face.variable_registry) if face.variable_registry else frozenset()
|
|
135
136
|
)
|
|
137
|
+
resolve_errors: dict[str, ChartResolveFailure] = {}
|
|
136
138
|
try:
|
|
137
|
-
resolved, _render_cache = build_resolved_face(
|
|
139
|
+
resolved, _render_cache = build_resolved_face(
|
|
140
|
+
face, executor, merged_variables, resolve_errors=resolve_errors
|
|
141
|
+
)
|
|
138
142
|
except DatafaceError as exc:
|
|
139
143
|
return EmitBoardArtifactResult(success=False, errors=[exc.to_diagnostic()])
|
|
140
144
|
finally:
|
|
141
145
|
set_auto_link_context(False)
|
|
142
146
|
set_filter_variables_context(frozenset())
|
|
143
147
|
|
|
148
|
+
# A chart-level resolve failure no longer raises — it leaves a hole in the
|
|
149
|
+
# face instead. Publishing that hole silently would be worse than the fatal
|
|
150
|
+
# it replaced, since a live render at least draws an error tile where an
|
|
151
|
+
# artifact just has nothing. Artifacts stay all-or-nothing.
|
|
152
|
+
if resolve_errors:
|
|
153
|
+
return EmitBoardArtifactResult(
|
|
154
|
+
success=False,
|
|
155
|
+
errors=[failure.diagnostic for failure in resolve_errors.values()],
|
|
156
|
+
)
|
|
157
|
+
|
|
144
158
|
recording = record_board(resolved, executor, merged_variables)
|
|
145
159
|
|
|
146
160
|
artifact_path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -11,7 +11,7 @@ AuthoredFace (dataface) definition from YAML.
|
|
|
11
11
|
| `tags` | list[str] | ✓ | Tags for categorization and search. |
|
|
12
12
|
| `aliases` | list[str] | ✓ | Additional URLs that redirect to this face's canonical file-path URL. Each entry must be absolute (leading /). Requests to these URLs are redirected (302) to the face's real path, query string preserved. Valid on .yml, .yaml, .md, and folder index.* faces. |
|
|
13
13
|
| `text` | str | ✓ | Markdown text content for text-only sections. |
|
|
14
|
-
| `
|
|
14
|
+
| `html_policy` | enum: "none", "safe-subset", "trusted-raw" | ✓ | HTML rendering policy for the face's body text. One of: "none" (default) — HTML is escaped and rendered as plain markdown; "safe-subset" — reserved for a parser-checked allowlist (not yet enforced — currently renders as none); "trusted-raw" — raw HTML via foreignObject. TRUSTED-CONTENT ONLY: this is NOT a security sandbox. <script>/event-handlers are stripped as a best-effort guard, not a guarantee. Enable only on first-party faces you fully control. |
|
|
15
15
|
| `source` | str | ✓ | Default source name for all queries in this face. Inheritable via meta.yaml cascade. |
|
|
16
16
|
| `cache` | [Cache](#cache) | ✓ | Cache policy for every query in this dashboard, e.g. cache: 1h — queries inherit it and may refine it; cache: false opts the whole dashboard out. Inheritable via the meta.yaml cascade. |
|
|
17
17
|
| `variables` | dict[str, [Variable](#variable) \| VariableRef] | ✓ | Variable definitions for dynamic filtering and UI controls. |
|
|
@@ -1898,9 +1898,9 @@ Authored overlay for TextColumnStyle. Author overrides for the column layout of
|
|
|
1898
1898
|
|
|
1899
1899
|
| Field | Type | Optional | Description |
|
|
1900
1900
|
|-------|------|:--------:|-------------|
|
|
1901
|
-
| `
|
|
1901
|
+
| `max_number` | int | ✓ | Ceiling on the column count. The renderer may choose fewer when there is not enough text to fill them. None = no ceiling. |
|
|
1902
1902
|
| `gap` | float | ✓ | Gap between columns in pixels. None = 1.5 line boxes, so the gutter scales with the type it separates. |
|
|
1903
|
-
| `rule` |
|
|
1903
|
+
| `rule` | [ColumnRuleStyle](#columnrulestyle) | ✓ | Vertical rule drawn between columns. None = no rule. |
|
|
1904
1904
|
| `max_chars` | int | ✓ | Column width as a character count, overriding the shipped measure. The width is used exactly and the column count follows from it. |
|
|
1905
1905
|
|
|
1906
1906
|
<a id="textcodestyle"></a>
|
|
@@ -2620,6 +2620,16 @@ Authored overlay for GeoshapeMarkStyle. Geoshape (choropleth) mark fill and boun
|
|
|
2620
2620
|
| `fill` | str | ✓ | Neutral geoshape fill color. |
|
|
2621
2621
|
| `stroke` | [StrokeStyle](#strokestyle) | ✓ | Geoshape boundary stroke style. |
|
|
2622
2622
|
|
|
2623
|
+
<a id="columnrulestyle"></a>
|
|
2624
|
+
## ColumnRuleStyle
|
|
2625
|
+
Authored overlay for ColumnRuleStyle. Vertical rule drawn between prose columns, structured like BorderStyle.
|
|
2626
|
+
|
|
2627
|
+
| Field | Type | Optional | Description |
|
|
2628
|
+
|-------|------|:--------:|-------------|
|
|
2629
|
+
| `width` | float | ✓ | Rule line width in pixels. |
|
|
2630
|
+
| `color` | str | ✓ | Rule color as a CSS color string. |
|
|
2631
|
+
| `style` | enum: "solid", "dashed", "dotted" | ✓ | Rule line style. |
|
|
2632
|
+
|
|
2623
2633
|
<a id="circlemarkstyle"></a>
|
|
2624
2634
|
## CircleMarkStyle
|
|
2625
2635
|
Authored overlay for CircleMarkStyle. Circle mark opacity and stroke.
|
|
@@ -2847,7 +2857,7 @@ Authored overlay for SliceLabelsStyle. Pie labels: typography + positioning offs
|
|
|
2847
2857
|
|-------|------|:--------:|-------------|
|
|
2848
2858
|
| `offset` | float | ✓ | Radial offset of slice labels from the arc in pixels. |
|
|
2849
2859
|
| `line_height` | float | ✓ | Line height for slice labels in pixels. Reserved vertical space above the disk is ``line_height × <rendered lines>`` per row, so the same value handles 1-line, 2-line, and multi-line templates. |
|
|
2850
|
-
| `font` | [FontStyle](#fontstyle) | ✓ | Slice label font style overrides. Unset fields fall back to [`style.charts.font`](#chartsstyle). |
|
|
2860
|
+
| `font` | [FontStyle](#fontstyle) | ✓ | Slice label font style overrides. ``color`` only takes effect on single-series pies (no ``color:`` channel authored); multi-series pies always paint each label the dark companion of its own wedge color and ignore an authored ``color`` here. Unset fields fall back to [`style.charts.font`](#chartsstyle) (except `color`). |
|
|
2851
2861
|
| `default_template` | [LabelsDefaultTemplate](#labelsdefaulttemplate) | ✓ | Default Jinja templates for per-slice labels when template is not authored. |
|
|
2852
2862
|
| `template` | str | ✓ | Jinja2 label template. Overrides default_template when authored. |
|
|
2853
2863
|
| `where` | str | ✓ | Jinja2 boolean filter — labels only render on rows where this is truthy. |
|
|
@@ -365,8 +365,11 @@ class ProjectSession:
|
|
|
365
365
|
self,
|
|
366
366
|
name: str,
|
|
367
367
|
path: Path,
|
|
368
|
+
vars: dict[str, Any] | None = None,
|
|
368
369
|
) -> _query.FaceQueryLookupResult:
|
|
369
|
-
return _query.lookup_face_query_sql(
|
|
370
|
+
return _query.lookup_face_query_sql(
|
|
371
|
+
name=name, path=path, project=self.project, vars=vars
|
|
372
|
+
)
|
|
370
373
|
|
|
371
374
|
def query_face(
|
|
372
375
|
self,
|
dataface/agent_api/query.py
CHANGED
|
@@ -33,12 +33,17 @@ def lookup_face_query_sql(
|
|
|
33
33
|
path: Path,
|
|
34
34
|
*,
|
|
35
35
|
project: Project,
|
|
36
|
+
vars: dict[str, Any] | None = None,
|
|
36
37
|
) -> FaceQueryLookupResult:
|
|
37
|
-
"""Compile a face file
|
|
38
|
+
"""Compile a face file, render the SQL template, and return it for offline use.
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
Expands ``{{ queries.X }}`` and substitutes variables so callers receive
|
|
41
|
+
valid SQL rather than a raw Jinja template. Used by validate and describe
|
|
42
|
+
paths that need the rendered SQL text without executing it against a
|
|
43
|
+
warehouse.
|
|
41
44
|
"""
|
|
45
|
+
from dataface.core.compile.parameterized import render_parameterized_with_queries
|
|
46
|
+
|
|
42
47
|
try:
|
|
43
48
|
file_path = resolve_face_path(path, project)
|
|
44
49
|
except ValueError as e:
|
|
@@ -74,7 +79,18 @@ def lookup_face_query_sql(
|
|
|
74
79
|
],
|
|
75
80
|
)
|
|
76
81
|
|
|
77
|
-
|
|
82
|
+
merged_vars = {**face.variable_defaults, **(vars or {})}
|
|
83
|
+
try:
|
|
84
|
+
rendered = render_parameterized_with_queries(
|
|
85
|
+
query.sql,
|
|
86
|
+
merged_vars,
|
|
87
|
+
queries=face.queries,
|
|
88
|
+
strict=not query.lenient_variables,
|
|
89
|
+
)
|
|
90
|
+
except (ValueError, KeyError, TypeError) as exc:
|
|
91
|
+
return FaceQueryLookupResult(success=False, errors=[str(exc)])
|
|
92
|
+
|
|
93
|
+
return FaceQueryLookupResult(success=True, sql=rendered.sql, source=query.source)
|
|
78
94
|
|
|
79
95
|
|
|
80
96
|
class ExecuteQueryArgs(BaseModel):
|
|
@@ -93,6 +109,14 @@ class ExecuteQueryArgs(BaseModel):
|
|
|
93
109
|
False,
|
|
94
110
|
description="When True, undefined Jinja variables degrade gracefully (useful for iterative chart editing where not all variables are set).",
|
|
95
111
|
)
|
|
112
|
+
# Display only — never reaches execute_query(). Ad-hoc SQL is the one query
|
|
113
|
+
# surface with no author to describe it: a named face query carries the
|
|
114
|
+
# `description:` its YAML already declares, and this is the equivalent for
|
|
115
|
+
# a query that exists only for the length of one tool call.
|
|
116
|
+
description: str | None = Field(
|
|
117
|
+
None,
|
|
118
|
+
description="Short phrase naming what this query is for (e.g. 'Leads by industry'), shown to the user in place of the raw SQL.",
|
|
119
|
+
)
|
|
96
120
|
|
|
97
121
|
|
|
98
122
|
class ExecuteQueryResult(BaseModel):
|
|
@@ -236,6 +260,9 @@ class QueryFaceResult(BaseModel):
|
|
|
236
260
|
path: PurePosixPath
|
|
237
261
|
query_type: str | None = None
|
|
238
262
|
sql: str | None = None
|
|
263
|
+
#: The query's authored `description:`, when its YAML declares one. Lets a
|
|
264
|
+
#: caller label the run with what it is for rather than its identifier.
|
|
265
|
+
description: str | None = None
|
|
239
266
|
columns: list[str] = []
|
|
240
267
|
data: list[dict[str, Any]] = []
|
|
241
268
|
row_count: int = 0
|
|
@@ -249,11 +276,19 @@ def _fail(
|
|
|
249
276
|
path: PurePosixPath,
|
|
250
277
|
errors: list[str],
|
|
251
278
|
available: list[str] | None = None,
|
|
279
|
+
sql: str | None = None,
|
|
280
|
+
description: str | None = None,
|
|
252
281
|
) -> QueryFaceResult:
|
|
282
|
+
"""A failure reports the query it was running whenever it got far enough to
|
|
283
|
+
know it: an undefined-variable error is unreadable without the SQL holding
|
|
284
|
+
the `{{ }}` that went unbound. Failures before the query is looked up
|
|
285
|
+
(unresolvable path, compile error, unknown name) have neither to report."""
|
|
253
286
|
return QueryFaceResult(
|
|
254
287
|
success=False,
|
|
255
288
|
name=name,
|
|
256
289
|
path=path,
|
|
290
|
+
sql=sql,
|
|
291
|
+
description=description,
|
|
257
292
|
errors=errors,
|
|
258
293
|
available_queries=available or [],
|
|
259
294
|
)
|
|
@@ -298,6 +333,8 @@ def query_face(
|
|
|
298
333
|
|
|
299
334
|
merged_vars = {**face.variable_defaults, **(vars or {})}
|
|
300
335
|
query = face.queries[name].model_copy(update={"limit": limit + 1})
|
|
336
|
+
sql = query.sql if isinstance(query, SqlQuery) else None
|
|
337
|
+
described = query.description
|
|
301
338
|
|
|
302
339
|
# Detect file sources before dispatching — the adapter path has no materializer
|
|
303
340
|
# and would fall through to SqlAdapter with a cryptic dbt error. File-source
|
|
@@ -324,6 +361,8 @@ def query_face(
|
|
|
324
361
|
"(dft render / dft serve). "
|
|
325
362
|
"Use dft render to preview this dashboard."
|
|
326
363
|
],
|
|
364
|
+
sql=sql,
|
|
365
|
+
description=described,
|
|
327
366
|
)
|
|
328
367
|
|
|
329
368
|
try:
|
|
@@ -331,10 +370,16 @@ def query_face(
|
|
|
331
370
|
query, variables=merged_vars or None, face=face, query_name=name
|
|
332
371
|
)
|
|
333
372
|
except Exception as e: # noqa: BLE001
|
|
334
|
-
return _fail(name, resolved_display, [str(e)])
|
|
373
|
+
return _fail(name, resolved_display, [str(e)], sql=sql, description=described)
|
|
335
374
|
|
|
336
375
|
if exec_result.error:
|
|
337
|
-
return _fail(
|
|
376
|
+
return _fail(
|
|
377
|
+
name,
|
|
378
|
+
resolved_display,
|
|
379
|
+
[exec_result.error],
|
|
380
|
+
sql=sql,
|
|
381
|
+
description=described,
|
|
382
|
+
)
|
|
338
383
|
|
|
339
384
|
data = normalize_data_for_json(exec_result.data)
|
|
340
385
|
truncated = len(data) > limit
|
|
@@ -345,7 +390,8 @@ def query_face(
|
|
|
345
390
|
name=name,
|
|
346
391
|
path=resolved_display,
|
|
347
392
|
query_type=query.query_type,
|
|
348
|
-
sql=
|
|
393
|
+
sql=sql,
|
|
394
|
+
description=described,
|
|
349
395
|
columns=list(data[0].keys()) if data else [],
|
|
350
396
|
data=data,
|
|
351
397
|
row_count=len(data),
|
|
@@ -260,12 +260,14 @@ Named presets (prefer these over raw D3 strings):
|
|
|
260
260
|
| Preset | Example output | Notes |
|
|
261
261
|
|--------|---------------|-------|
|
|
262
262
|
| `integer` | `1,234` | |
|
|
263
|
+
| `year` | `2025` — no grouping, no SI scaling | an identifier, not a quantity |
|
|
263
264
|
| `currency_whole` | `$1,234` | |
|
|
264
265
|
| `currency_compact` | `$1.2M` | |
|
|
265
266
|
| `percent` | `2.3%` — input is decimal fraction (0.023) | line/bar/area/scatter via `style.number_format` |
|
|
266
267
|
| `percent_number` | `2.3%` — input is whole-number percent (2.3) | **KPI only** (`style.value.format`) |
|
|
267
268
|
| `percent_delta` | `+2.3%` — input is decimal fraction | |
|
|
268
269
|
| `percent_number_delta` | `+2.3%` — input is whole-number (2.3) | **KPI only** |
|
|
270
|
+
| `percentage_points_delta` | `+3.2 pts` — the difference of two percentages | for a point delta, not a ratio delta |
|
|
269
271
|
|
|
270
272
|
**`percent_number` and `percent_number_delta` only work on `type: kpi`.** Using them on line/bar/area/scatter causes a render error. For those chart types use `percent` or `percent_delta` (decimal-fraction input) or omit formatting and accept axis defaults.
|
|
271
273
|
|
dataface/ai/tools/__init__.py
CHANGED
|
@@ -206,9 +206,16 @@ def _handle_get_skill(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str,
|
|
|
206
206
|
)
|
|
207
207
|
except _skills.SkillNotFound as exc:
|
|
208
208
|
return {"success": False, "errors": [str(exc)]}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
)
|
|
209
|
+
# `Skill` is a domain model, not a result envelope — it carries no verdict
|
|
210
|
+
# of its own, and it must not grow one (it nests inside `SkillList.skills`,
|
|
211
|
+
# where a per-item `success` means nothing). The envelope is added here, so
|
|
212
|
+
# this payload states its outcome like every other tool's.
|
|
213
|
+
return {
|
|
214
|
+
"success": True,
|
|
215
|
+
**skill.model_dump(
|
|
216
|
+
mode="json", exclude_none=True, exclude=_skills.SKILL_WIRE_EXCLUDE_FIELDS
|
|
217
|
+
),
|
|
218
|
+
}
|
|
212
219
|
|
|
213
220
|
|
|
214
221
|
def _handle_search_skills(
|
dataface/cli/commands/query.py
CHANGED
|
@@ -344,7 +344,7 @@ def query_command(
|
|
|
344
344
|
if mode_is_named:
|
|
345
345
|
assert query_name is not None
|
|
346
346
|
assert face is not None
|
|
347
|
-
lr = project_session.lookup_face_query_sql(query_name, face)
|
|
347
|
+
lr = project_session.lookup_face_query_sql(query_name, face, vars=vars)
|
|
348
348
|
if not lr.success:
|
|
349
349
|
_handle_lookup_failure(lr, query_name)
|
|
350
350
|
sql_text = lr.sql
|
|
@@ -16,7 +16,7 @@ helpers) live there too.
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
18
|
from dataface.core.compile.merge import merge_onto_base
|
|
19
|
-
from dataface.core.compile.models.style.
|
|
19
|
+
from dataface.core.compile.models.style.context import ChartStyleContext
|
|
20
20
|
from dataface.core.compile.models.style.theme import DataTableStyle
|
|
21
21
|
from dataface.core.compile.style_cascade import resolved_axis_style
|
|
22
22
|
|
|
@@ -31,7 +31,7 @@ def row_height(style: DataTableStyle) -> float:
|
|
|
31
31
|
return _ROW_HEIGHT_BASE + padding.vertical * 2.0
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
def axis_offset(
|
|
34
|
+
def axis_offset(chart_style_context: ChartStyleContext, style: DataTableStyle) -> float:
|
|
35
35
|
"""Pixel offset from plot bottom edge to the bottom of the x-axis block.
|
|
36
36
|
|
|
37
37
|
Sums whichever axis components are actually visible: a chart with the
|
|
@@ -45,14 +45,14 @@ def axis_offset(charts_style: ResolvedChartsStyle, style: DataTableStyle) -> flo
|
|
|
45
45
|
|
|
46
46
|
MUST use the fully-merged axis (resolved_axis_style) rather than reading
|
|
47
47
|
charts_style.axis_x directly. The authored x_label visibility flip is
|
|
48
|
-
stored on axis_overrides_x (Layer 6) by
|
|
48
|
+
stored on axis_overrides_x (Layer 6) by build_chart_style_context; reading
|
|
49
49
|
the base axis_x (Layers 1+2) misses it, under-reserving title space and
|
|
50
50
|
causing the strip to collide with the rendered axis title.
|
|
51
51
|
"""
|
|
52
52
|
# Font metrics come from the base ResolvedAxisStyle (non-None guaranteed).
|
|
53
53
|
# Visibility is read from the fully-merged cascade (includes Layer-6 authored-
|
|
54
54
|
# label flips) so both label and title heights reflect the actual rendered state.
|
|
55
|
-
merged = resolved_axis_style(
|
|
55
|
+
merged = resolved_axis_style(chart_style_context, "axis_x", "band")
|
|
56
56
|
# labels.padding is always required by build_resolved_axis (the shared
|
|
57
57
|
# ResolvedAxisElementStyle dataclass types it Optional only to let title
|
|
58
58
|
# leave it unset — see AxisTitleStyle).
|
|
@@ -72,7 +72,7 @@ def axis_offset(charts_style: ResolvedChartsStyle, style: DataTableStyle) -> flo
|
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
def resolve_effective_data_table_style(
|
|
75
|
-
|
|
75
|
+
chart_style_context: ChartStyleContext, chart_type: str
|
|
76
76
|
) -> DataTableStyle:
|
|
77
77
|
"""Resolve the effective data_table style for a chart.
|
|
78
78
|
|
|
@@ -81,7 +81,7 @@ def resolve_effective_data_table_style(
|
|
|
81
81
|
fields from charts.data_table, so this function only needs to apply the
|
|
82
82
|
per-family theme values (tier-2) on top.
|
|
83
83
|
"""
|
|
84
|
-
per_type = getattr(
|
|
84
|
+
per_type = getattr(chart_style_context, chart_type, None)
|
|
85
85
|
if per_type is None or not hasattr(per_type, "data_table"):
|
|
86
|
-
return
|
|
87
|
-
return merge_onto_base(
|
|
86
|
+
return chart_style_context.data_table
|
|
87
|
+
return merge_onto_base(chart_style_context.data_table, per_type.data_table)
|
dataface/core/compile/format.py
CHANGED
|
@@ -13,6 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
from typing import Any
|
|
14
14
|
|
|
15
15
|
from dataface.core.compile.models.primitives import FormatConfig
|
|
16
|
+
from dataface.core.text.format_d3 import round_aware_format
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
def resolve_format(
|
|
@@ -35,6 +36,15 @@ def resolve_format(
|
|
|
35
36
|
rendering. That pass keys slots off field name, so it is not a guarantee
|
|
36
37
|
for every value reaching this function.
|
|
37
38
|
|
|
39
|
+
The resolved spec also carries ``round_aware_format``'s ``~``-trim
|
|
40
|
+
injection (a no-op for a non-SI spec, a native-formatter key, or a
|
|
41
|
+
strftime directive). This is the single point nearly every consumer of a
|
|
42
|
+
resolved format string is baked from — axes and mark labels at compile,
|
|
43
|
+
KPI/table/tooltip at render — so every one of them, including a spec
|
|
44
|
+
handed straight to Vega-Lite (``axis.format``, a value label's
|
|
45
|
+
``text.format``), agrees on digits by construction instead of each
|
|
46
|
+
render leaf re-applying the trim itself.
|
|
47
|
+
|
|
38
48
|
Args:
|
|
39
49
|
format_input: Format specification (string, FormatConfig, dict, or None)
|
|
40
50
|
formats: The resolved format alias dict from the theme cascade
|
|
@@ -59,10 +69,10 @@ def resolve_format(
|
|
|
59
69
|
|
|
60
70
|
# Theme/face alias lookup (case-sensitive, key-exact)
|
|
61
71
|
if formats and format_str in formats:
|
|
62
|
-
return formats[format_str]
|
|
72
|
+
return round_aware_format(formats[format_str])
|
|
63
73
|
|
|
64
74
|
# Raw d3 passthrough — let d3 parse or raise
|
|
65
|
-
return format_str
|
|
75
|
+
return round_aware_format(format_str)
|
|
66
76
|
|
|
67
77
|
|
|
68
78
|
# d3's first SI prefix ("k") engages at 1000. Below it, ".2s" doesn't compact —
|
|
@@ -97,11 +97,16 @@ def _build_graph(
|
|
|
97
97
|
slot_at: StylePath | None = None,
|
|
98
98
|
slot_from: StylePath | None = None,
|
|
99
99
|
skip_slots: bool = False,
|
|
100
|
+
slot_exclude: frozenset[str] = frozenset(),
|
|
100
101
|
) -> None:
|
|
101
102
|
"""Walk *model_cls* recursively and populate *graph* from Inherit/InheritSlot markers.
|
|
102
103
|
|
|
103
104
|
``skip_slots=True`` suppresses InheritSlot expansion for the entire subtree —
|
|
104
105
|
used when a shared model's slot annotations must not apply at a particular path.
|
|
106
|
+
|
|
107
|
+
``slot_exclude`` carries the active slot's ``InheritSlot.exclude`` leaf names
|
|
108
|
+
down through the recursion; a leaf whose field name is in this set gets no
|
|
109
|
+
graph entry, leaving it a genuine cascade-managed sentinel.
|
|
105
110
|
"""
|
|
106
111
|
if model_cls in seen:
|
|
107
112
|
return
|
|
@@ -160,6 +165,7 @@ def _build_graph(
|
|
|
160
165
|
slot_at=slot_at if skip_marker.cascade else None,
|
|
161
166
|
slot_from=slot_from if skip_marker.cascade else None,
|
|
162
167
|
skip_slots=not skip_marker.cascade,
|
|
168
|
+
slot_exclude=slot_exclude if skip_marker.cascade else frozenset(),
|
|
163
169
|
)
|
|
164
170
|
elif slot_marker is not None and not skip_slots:
|
|
165
171
|
if slot_at is not None:
|
|
@@ -176,6 +182,7 @@ def _build_graph(
|
|
|
176
182
|
graph,
|
|
177
183
|
slot_at=slot_at,
|
|
178
184
|
slot_from=slot_from,
|
|
185
|
+
slot_exclude=slot_exclude,
|
|
179
186
|
)
|
|
180
187
|
else:
|
|
181
188
|
from_path = slot_marker.from_path
|
|
@@ -184,6 +191,12 @@ def _build_graph(
|
|
|
184
191
|
raise ValueError(
|
|
185
192
|
f"InheritSlot from_path {from_path!r} does not match any path in the model"
|
|
186
193
|
)
|
|
194
|
+
unknown_exclude = slot_marker.exclude - nested.model_fields.keys()
|
|
195
|
+
if unknown_exclude:
|
|
196
|
+
raise ValueError(
|
|
197
|
+
f"InheritSlot exclude {sorted(unknown_exclude)!r} on "
|
|
198
|
+
f"{_path_str(current)!r} not a field of {nested.__name__}"
|
|
199
|
+
)
|
|
187
200
|
# Nullable InheritSlot (T | None): emit a container-level copy
|
|
188
201
|
# link so apply_inherit can copy the whole parent object when
|
|
189
202
|
# this field is None. apply_inherit._set() skips writes through
|
|
@@ -200,6 +213,7 @@ def _build_graph(
|
|
|
200
213
|
graph,
|
|
201
214
|
slot_at=current,
|
|
202
215
|
slot_from=from_tuple,
|
|
216
|
+
slot_exclude=slot_marker.exclude,
|
|
203
217
|
)
|
|
204
218
|
else:
|
|
205
219
|
_build_graph(
|
|
@@ -211,6 +225,7 @@ def _build_graph(
|
|
|
211
225
|
slot_at=slot_at,
|
|
212
226
|
slot_from=slot_from,
|
|
213
227
|
skip_slots=skip_slots,
|
|
228
|
+
slot_exclude=slot_exclude,
|
|
214
229
|
)
|
|
215
230
|
else:
|
|
216
231
|
# Leaf node.
|
|
@@ -226,7 +241,13 @@ def _build_graph(
|
|
|
226
241
|
# slot (which authors may override independently).
|
|
227
242
|
# 2. Explicit Inherit: fires at canonical (non-slot) positions only,
|
|
228
243
|
# establishing the "ultimate fallback" for the entire slot family.
|
|
229
|
-
|
|
244
|
+
#
|
|
245
|
+
# A leaf named in the active slot's exclude set gets no link at
|
|
246
|
+
# all: it stays a genuine cascade-managed sentinel (None unless
|
|
247
|
+
# some tier's raw YAML sets it) rather than being backfilled.
|
|
248
|
+
if field_name in slot_exclude:
|
|
249
|
+
pass
|
|
250
|
+
elif slot_at is not None and slot_from is not None and skip_marker is None:
|
|
230
251
|
leaf_suffix: StylePath = current[len(slot_at) :]
|
|
231
252
|
fallback_path = _path_str(slot_from + leaf_suffix)
|
|
232
253
|
if fallback_path not in valid_paths:
|
|
@@ -4,7 +4,7 @@ apply_inherit fills None-sentinel leaves by walking the single-link
|
|
|
4
4
|
parent chains declared via Inherit/InheritSlot markers on the compiled Style.
|
|
5
5
|
It runs after merge_onto_base, replacing the former hard-coded push loop approach.
|
|
6
6
|
|
|
7
|
-
Not in scope:
|
|
7
|
+
Not in scope: _build_chart_style_context, resolved_axis_style.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
@@ -72,6 +72,9 @@ class SchemaField:
|
|
|
72
72
|
)
|
|
73
73
|
inherit_from: tuple[str, ...] = () # ordered fallback dot-paths (Inherit marker)
|
|
74
74
|
inherit_slot: str | None = None # source subtree path (InheritSlot marker)
|
|
75
|
+
inherit_slot_exclude: frozenset[str] = (
|
|
76
|
+
frozenset()
|
|
77
|
+
) # leaf names InheritSlot excludes
|
|
75
78
|
|
|
76
79
|
|
|
77
80
|
@dataclasses.dataclass
|
|
@@ -411,6 +414,9 @@ def _build_schema_field(
|
|
|
411
414
|
container_mapping_models=_container_mapping_models(annotation),
|
|
412
415
|
inherit_from=(inherit.from_path,) if inherit is not None else (),
|
|
413
416
|
inherit_slot=inherit_slot.from_path if inherit_slot is not None else None,
|
|
417
|
+
inherit_slot_exclude=(
|
|
418
|
+
inherit_slot.exclude if inherit_slot is not None else frozenset()
|
|
419
|
+
),
|
|
414
420
|
)
|
|
415
421
|
|
|
416
422
|
|
|
@@ -44,6 +44,13 @@ class CalloutChart(BaseModel):
|
|
|
44
44
|
description="Always None — callout has no query. Present so build_resolved_face "
|
|
45
45
|
"can access compiled.query_name directly without isinstance guards.",
|
|
46
46
|
)
|
|
47
|
+
description: str = Field(
|
|
48
|
+
default="",
|
|
49
|
+
description="Always empty — callout has no description. Present so generic "
|
|
50
|
+
"chart-iteration code (ChartIdentity.from_normalized) can access "
|
|
51
|
+
"chart.description without isinstance guards, mirroring the same "
|
|
52
|
+
"structural-parity field on ResolvedCalloutChart.",
|
|
53
|
+
)
|
|
47
54
|
query_is_inline: bool = Field(
|
|
48
55
|
default=False,
|
|
49
56
|
description="True when query is defined inline; False for named reference.",
|
|
@@ -30,6 +30,7 @@ from dataface.core.compile.models.style.resolved import (
|
|
|
30
30
|
ResolvedLegendStyle,
|
|
31
31
|
)
|
|
32
32
|
from dataface.core.compile.models.style.theme.board import PaddingStyle, TitleStyle
|
|
33
|
+
from dataface.core.compile.models.style.theme.table import DataTableStyle
|
|
33
34
|
from dataface.core.compile.models.vega_lite.contracts import Projection
|
|
34
35
|
|
|
35
36
|
|
|
@@ -189,6 +190,18 @@ class _CartesianResolvedChartFields(_SharedResolvedChartFields):
|
|
|
189
190
|
)
|
|
190
191
|
min_height: float | None = Field(default=None, description="Minimum chart height.")
|
|
191
192
|
max_height: float | None = Field(default=None, description="Maximum chart height.")
|
|
193
|
+
# Baked at resolve time so render never re-runs the axis cascade to
|
|
194
|
+
# compute data_table strip geometry — see compile/data_table.py's
|
|
195
|
+
# axis_offset()/resolve_effective_data_table_style(), the per-chart
|
|
196
|
+
# cascade steps this projects. None when data_table is None.
|
|
197
|
+
data_table_axis_offset: float | None = Field(
|
|
198
|
+
default=None,
|
|
199
|
+
description="Pixel offset from plot bottom to the x-axis block bottom, for data_table strip placement.",
|
|
200
|
+
)
|
|
201
|
+
effective_data_table_style: DataTableStyle | None = Field(
|
|
202
|
+
default=None,
|
|
203
|
+
description="Final per-chart data_table style (board theme merged with the family override).",
|
|
204
|
+
)
|
|
192
205
|
|
|
193
206
|
|
|
194
207
|
class _GeoResolvedChartFields(_SharedResolvedChartFields):
|
|
@@ -162,22 +162,6 @@ class ChartRenderingConfig(ConfigNode):
|
|
|
162
162
|
footer_rule_gap_px: int
|
|
163
163
|
footer_timestamp_gap_px: int
|
|
164
164
|
|
|
165
|
-
class ProseConfig(ConfigNode):
|
|
166
|
-
target_chars: int
|
|
167
|
-
single_column_chars: int
|
|
168
|
-
min_chars: int
|
|
169
|
-
max_chars: int
|
|
170
|
-
min_lines_per_column: int
|
|
171
|
-
max_columns: int
|
|
172
|
-
gutter_line_boxes: float
|
|
173
|
-
orphan_penalty: float
|
|
174
|
-
widow_penalty: float
|
|
175
|
-
interior_break_penalty: float
|
|
176
|
-
block_break_penalty: float
|
|
177
|
-
balance_weight: float
|
|
178
|
-
balance_scale: float
|
|
179
|
-
balance_undershoot: float
|
|
180
|
-
|
|
181
165
|
class DataTableConfig(ConfigNode):
|
|
182
166
|
divider_gap: float
|
|
183
167
|
chart_data_table_max_x_ticks: int
|
|
@@ -190,7 +174,6 @@ class ChartRenderingConfig(ConfigNode):
|
|
|
190
174
|
bar: BarConfig
|
|
191
175
|
type_inference: TypeInferenceConfig
|
|
192
176
|
board: BoardConfig
|
|
193
|
-
prose: ProseConfig
|
|
194
177
|
data_table: DataTableConfig
|
|
195
178
|
stroke: StrokeConfig
|
|
196
179
|
axis: AxisConfig
|
|
@@ -441,14 +441,19 @@ class AuthoredFace(_FaceDesugarMixin):
|
|
|
441
441
|
text: Annotated[str | None, Merge(Strategy.OVERRIDE)] = Field(
|
|
442
442
|
default=None, description="Markdown text content for text-only sections."
|
|
443
443
|
)
|
|
444
|
-
|
|
445
|
-
|
|
444
|
+
html_policy: Annotated[
|
|
445
|
+
Literal["none", "safe-subset", "trusted-raw"], Merge(Strategy.OVERRIDE)
|
|
446
|
+
] = Field(
|
|
447
|
+
default="none",
|
|
446
448
|
description=(
|
|
447
|
-
"
|
|
448
|
-
"
|
|
449
|
-
"
|
|
450
|
-
"
|
|
451
|
-
"
|
|
449
|
+
"HTML rendering policy for the face's body text. One of: "
|
|
450
|
+
'"none" (default) — HTML is escaped and rendered as plain markdown; '
|
|
451
|
+
'"safe-subset" — reserved for a parser-checked allowlist (not yet '
|
|
452
|
+
"enforced — currently renders as none); "
|
|
453
|
+
'"trusted-raw" — raw HTML via foreignObject. TRUSTED-CONTENT ONLY: '
|
|
454
|
+
"this is NOT a security sandbox. <script>/event-handlers are stripped "
|
|
455
|
+
"as a best-effort guard, not a guarantee. Enable only on first-party "
|
|
456
|
+
"faces you fully control."
|
|
452
457
|
),
|
|
453
458
|
)
|
|
454
459
|
|
|
@@ -12,6 +12,7 @@ from dataface.core.compile.models.chart.normalized import Chart
|
|
|
12
12
|
from dataface.core.compile.models.face.authored import LayoutType
|
|
13
13
|
from dataface.core.compile.models.query.normalized import AnyQuery
|
|
14
14
|
from dataface.core.compile.models.style.authored import StylePatch
|
|
15
|
+
from dataface.core.compile.models.style.context import ChartStyleContext
|
|
15
16
|
from dataface.core.compile.models.style.resolved import ResolvedStyle
|
|
16
17
|
from dataface.core.compile.models.variable.authored import SingleRowBoolProbe, Variable
|
|
17
18
|
|
|
@@ -317,14 +318,11 @@ class Face(BaseModel):
|
|
|
317
318
|
text: str = Field(
|
|
318
319
|
default="", description="Markdown text content for text-only faces."
|
|
319
320
|
)
|
|
320
|
-
|
|
321
|
-
default=
|
|
321
|
+
html_policy: Literal["none", "safe-subset", "trusted-raw"] = Field(
|
|
322
|
+
default="none",
|
|
322
323
|
description=(
|
|
323
|
-
"
|
|
324
|
-
"markdown.
|
|
325
|
-
"interpolated values) is rendered as-authored — this is NOT a security "
|
|
326
|
-
"sandbox. mdsvg strips <script>/event-handlers as a best-effort guard, "
|
|
327
|
-
"not a guarantee. Enable only on first-party faces you fully control."
|
|
324
|
+
"HTML rendering policy for the face's body text. "
|
|
325
|
+
"See docs/contributing/markdown-html-policy.md for tier definitions."
|
|
328
326
|
),
|
|
329
327
|
)
|
|
330
328
|
|
|
@@ -379,6 +377,15 @@ class Face(BaseModel):
|
|
|
379
377
|
resolved_style: ResolvedStyle = Field(
|
|
380
378
|
description="Board-scoped resolved style; produced at compile time. Render reads this instead of re-merging patches.",
|
|
381
379
|
)
|
|
380
|
+
# Board-scoped chart style cascade context — compiler working state
|
|
381
|
+
# (sparse axis overlays, patch sentinels, palette/role token bindings, the
|
|
382
|
+
# pre-inherit tree), produced alongside resolved_style by the same
|
|
383
|
+
# cascade. Used only by runtime chart resolution (execute orchestration,
|
|
384
|
+
# sizing, the data_table-attachment axis-offset step) — never by
|
|
385
|
+
# ResolvedFace or any mechanical render API.
|
|
386
|
+
chart_style_context: ChartStyleContext = Field(
|
|
387
|
+
description="Board-scoped chart style cascade context for runtime chart resolution.",
|
|
388
|
+
)
|
|
382
389
|
|
|
383
390
|
@property
|
|
384
391
|
def visible_variables(self) -> "dict[str, Variable]":
|