dataface 0.4.1.dev5__py3-none-any.whl → 0.4.1.dev53__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 +12 -0
- dataface/agent_api/diagnostics.py +25 -7
- dataface/agent_api/docs/yaml-reference.md +4 -9
- dataface/ai/skills/dashboard-pack-scaffolding/SKILL.md +1 -1
- dataface/cli/_error_format.py +13 -9
- dataface/cli/commands/render.py +25 -7
- dataface/cli/main.py +8 -8
- dataface/core/compile/chart_focus.py +13 -2
- dataface/core/compile/chart_update.py +23 -19
- dataface/core/compile/jinja.py +19 -3
- dataface/core/compile/migrations/versions/v0_4_0.py +5 -5
- dataface/core/compile/models/chart/authored/__init__.py +0 -2
- dataface/core/compile/models/chart/authored/_conditional_formatting.py +2 -18
- dataface/core/compile/models/config.py +17 -0
- dataface/core/compile/models/style/resolved/_base.py +0 -1
- dataface/core/compile/models/style/resolved/_cascade.py +0 -1
- dataface/core/compile/models/style/theme/__init__.py +0 -2
- dataface/core/compile/models/style/theme/axis.py +0 -11
- dataface/core/compile/models/style/theme/board.py +21 -67
- dataface/core/compile/models/style/theme/variables.py +0 -19
- dataface/core/compile/parameterized.py +59 -1
- dataface/core/compile/resolve/__init__.py +2 -4
- dataface/core/compile/resolve/_dispatch.py +2 -95
- dataface/core/compile/resolve/_domain.py +1 -68
- dataface/core/compile/resolve/area.py +5 -19
- dataface/core/compile/resolve/bar.py +0 -15
- dataface/core/compile/resolve/line.py +5 -20
- dataface/core/compile/resolve/pie.py +52 -53
- dataface/core/compile/resolve/pie_attachment.py +56 -1
- dataface/core/compile/resolve/scatter.py +18 -31
- dataface/core/compile/sizing.py +239 -5
- dataface/core/dashboard.py +1 -6
- dataface/core/defaults/default_config.yml +39 -0
- dataface/core/defaults/themes/_base.yaml +9 -17
- dataface/core/defaults/themes/cream.yaml +0 -3
- dataface/core/defaults/themes/neon.yaml +0 -1
- dataface/core/diagnostics/__init__.py +0 -2
- dataface/core/diagnostics/codes_compile.py +5 -0
- dataface/core/diagnostics/codes_execute.py +0 -23
- dataface/core/diagnostics/codes_render.py +1 -0
- dataface/core/diagnostics/registry.py +6 -0
- dataface/core/execute/chart_resolution.py +5 -199
- dataface/core/font_measure.py +20 -0
- dataface/core/render/__init__.py +0 -2
- dataface/core/render/chart/kpi.py +17 -13
- dataface/core/render/chart/rendering.py +7 -7
- dataface/core/render/chart/table.py +4 -2
- dataface/core/render/chart/table_support.py +3 -3
- dataface/core/render/column_packer.py +236 -0
- dataface/core/render/controls.py +26 -476
- dataface/core/render/converters/chart.py +20 -0
- dataface/core/render/converters/html.py +5 -8
- dataface/core/render/faces.py +61 -224
- dataface/core/render/layout_sizing.py +2 -19
- dataface/core/render/prose.py +295 -0
- dataface/core/render/render_result.py +0 -3
- dataface/core/render/renderer.py +7 -25
- dataface/core/render/sizing.py +56 -316
- dataface/core/render/svg_utils.py +25 -6
- dataface/core/render/templates/controls/_styles.css +50 -489
- dataface/core/render/templates/page.html +1 -2
- dataface/core/render/templates/scripts/variables.js +776 -1028
- dataface/core/render/templates/svg/styles.css +29 -6
- dataface/core/render/variables_resolve.py +59 -6
- dataface/core/render/variables_strip.py +119 -9
- dataface/core/render/warnings/layered_chart_shared_y_axis_scale_mismatch.py +2 -1
- dataface/core/utils.py +7 -3
- dataface/data/schemas/board-resolved/board-resolved.schema.json +5 -85
- {dataface-0.4.1.dev5.dist-info → dataface-0.4.1.dev53.dist-info}/METADATA +1 -2
- {dataface-0.4.1.dev5.dist-info → dataface-0.4.1.dev53.dist-info}/RECORD +76 -84
- mdsvg/fonts.py +28 -2
- mdsvg/renderer.py +132 -2
- mdsvg/style.py +6 -0
- dataface/core/render/templates/variable_controls/checkbox.html +0 -6
- dataface/core/render/templates/variable_controls/date.html +0 -6
- dataface/core/render/templates/variable_controls/daterange.html +0 -16
- dataface/core/render/templates/variable_controls/layer.html +0 -28
- dataface/core/render/templates/variable_controls/multiselect.html +0 -19
- dataface/core/render/templates/variable_controls/number.html +0 -6
- dataface/core/render/templates/variable_controls/readonly.html +0 -4
- dataface/core/render/templates/variable_controls/select.html +0 -13
- dataface/core/render/templates/variable_controls/slider.html +0 -9
- dataface/core/render/templates/variable_controls/text.html +0 -6
- {dataface-0.4.1.dev5.dist-info → dataface-0.4.1.dev53.dist-info}/WHEEL +0 -0
- {dataface-0.4.1.dev5.dist-info → dataface-0.4.1.dev53.dist-info}/entry_points.txt +0 -0
- {dataface-0.4.1.dev5.dist-info → dataface-0.4.1.dev53.dist-info}/licenses/LICENSE +0 -0
dataface/DATAFACE_SYNTAX.md
CHANGED
|
@@ -957,6 +957,18 @@ WHERE plan IN ({{ plans | map('tojson') | join(', ') }})
|
|
|
957
957
|
WHERE {{ filter('plan', plans) }}
|
|
958
958
|
```
|
|
959
959
|
|
|
960
|
+
**Numeric arithmetic antipattern** — `{{ n | int }}` and `{{ n | float }}` raise an error on parameterized variables. Jinja's `| int` filter calls `int(value)` internally; on a parameterized variable this would silently return 0, so Dataface rejects it instead. Write SQL arithmetic directly on the variable:
|
|
961
|
+
|
|
962
|
+
```sql
|
|
963
|
+
-- WRONG: raises ERR-JINJA-ERROR at render time
|
|
964
|
+
INTERVAL -({{ months | int - 1 }}) MONTH
|
|
965
|
+
|
|
966
|
+
-- CORRECT: {{ months }} emits the bound parameter; the database handles arithmetic
|
|
967
|
+
INTERVAL -({{ months }} - 1) MONTH
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
In a direct query (no `{{ queries.X }}` references), `{{ n }}` emits a bound parameter (`$1`, `%s`, `?` depending on dialect). In a composed query (one using `{{ queries.X }}`), variables are resolved as literals before the parameterization pass — `{{ n }}` with `n: 3` becomes the literal `3`. In both cases, `{{ n | int }}` and `{{ n | float }}` raise — so adding or removing a `{{ queries.X }}` reference cannot change the value a filter expression produces. Arithmetic, casting, and type coercion belong in the SQL expression around the variable, not in a Jinja filter.
|
|
971
|
+
|
|
960
972
|
Common mistakes (the validator rejects these — listed here so authors don't hit them):
|
|
961
973
|
|
|
962
974
|
- `options.values: [...]` → use `options.static: [...]`.
|
|
@@ -36,9 +36,10 @@ __all__ = [
|
|
|
36
36
|
"DiagnosticCodeDetail",
|
|
37
37
|
"DiagnosticCodesListResult",
|
|
38
38
|
"DiagnosticCodeDetailResult",
|
|
39
|
+
"DiagnosticDisplay",
|
|
39
40
|
"list_diagnostic_codes",
|
|
40
41
|
"get_diagnostic_code",
|
|
41
|
-
"
|
|
42
|
+
"display_map",
|
|
42
43
|
]
|
|
43
44
|
|
|
44
45
|
# ----- Args types ------------------------------------------------------------
|
|
@@ -77,6 +78,20 @@ class DiagnosticCodeSummary(BaseModel):
|
|
|
77
78
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
78
79
|
|
|
79
80
|
|
|
81
|
+
class DiagnosticDisplay(BaseModel):
|
|
82
|
+
"""Registry-derived presentation data for one code, shipped per page.
|
|
83
|
+
|
|
84
|
+
Both fields are derived DiagnosticCode properties rather than serialized
|
|
85
|
+
Diagnostic fields, so a display surface resolves them from the code it
|
|
86
|
+
already has instead of receiving them on every payload.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
title: str
|
|
90
|
+
doc_url: str
|
|
91
|
+
|
|
92
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
93
|
+
|
|
94
|
+
|
|
80
95
|
class DiagnosticCodeDetail(BaseModel):
|
|
81
96
|
"""Full details for a single diagnostic code."""
|
|
82
97
|
|
|
@@ -207,11 +222,14 @@ def get_diagnostic_code(code: str) -> DiagnosticCodeDetailResult:
|
|
|
207
222
|
)
|
|
208
223
|
|
|
209
224
|
|
|
210
|
-
def
|
|
211
|
-
"""The registry's code ->
|
|
225
|
+
def display_map() -> dict[str, DiagnosticDisplay]:
|
|
226
|
+
"""The registry's code -> display map, built once for embedding alongside a page.
|
|
212
227
|
|
|
213
|
-
doc_url
|
|
214
|
-
|
|
215
|
-
re-deriving or
|
|
228
|
+
`title` and `doc_url` are derived DiagnosticCode properties, never
|
|
229
|
+
authored/serialized Diagnostic fields — Cloud and Playground each ship this
|
|
230
|
+
map once rather than re-deriving a URL or restating a title per diagnostic.
|
|
216
231
|
"""
|
|
217
|
-
return {
|
|
232
|
+
return {
|
|
233
|
+
dc.code: DiagnosticDisplay(title=dc.title, doc_url=dc.doc_url)
|
|
234
|
+
for dc in REGISTRY.all()
|
|
235
|
+
}
|
|
@@ -1177,13 +1177,10 @@ Authored overlay for VariablesStyle. Variable controls chrome styling.
|
|
|
1177
1177
|
| `label` | [VariablesLabelStyle](#variableslabelstyle) | ✓ | Variable label typography. |
|
|
1178
1178
|
| `value` | [VariablesValueStyle](#variablesvaluestyle) | ✓ | Variable value typography. |
|
|
1179
1179
|
| `placeholder` | [VariablesPlaceholderStyle](#variablesplaceholderstyle) | ✓ | Style for unselected/hint text in variable inputs. |
|
|
1180
|
-
| `padding` | float | ✓ | Outer padding around the variables panel in pixels. |
|
|
1181
1180
|
| `container_height` | float | ✓ | Height of the variables panel container in pixels. |
|
|
1182
|
-
| `container_padding` | float | ✓ | Inner padding of the variables panel container in pixels. |
|
|
1183
1181
|
| `border` | [BorderStyle](#borderstyle) | ✓ | Variables panel border style. |
|
|
1184
1182
|
| `control_gap` | float | ✓ | Gap between label and input within a single control in pixels. |
|
|
1185
1183
|
| `input` | [InputStyle](#inputstyle) | ✓ | Input control style. |
|
|
1186
|
-
| `popover_rail_background` | str | ✓ | Background color for the preset rail (left column) inside the daterange chip's popover. Typically the theme's `surface-subtle` step — gray-50 on white-canvas themes, a cream-toned light on warm-canvas themes. Distinct from the popover card itself, which is always white-equivalent. |
|
|
1187
1184
|
|
|
1188
1185
|
<a id="pagestyle"></a>
|
|
1189
1186
|
## PageStyle
|
|
@@ -1897,14 +1894,14 @@ Authored overlay for BlockMarginStyle. Top and bottom margin for a prose block r
|
|
|
1897
1894
|
|
|
1898
1895
|
<a id="textcolumnstyle"></a>
|
|
1899
1896
|
## TextColumnStyle
|
|
1900
|
-
Authored overlay for TextColumnStyle.
|
|
1897
|
+
Authored overlay for TextColumnStyle. Author overrides for the column layout of face body text.
|
|
1901
1898
|
|
|
1902
1899
|
| Field | Type | Optional | Description |
|
|
1903
1900
|
|-------|------|:--------:|-------------|
|
|
1904
|
-
| `number` | int | ✓ |
|
|
1905
|
-
| `gap` | float | ✓ | Gap between columns in pixels. None =
|
|
1901
|
+
| `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
|
+
| `gap` | float | ✓ | Gap between columns in pixels. None = 1.5 line boxes, so the gutter scales with the type it separates. |
|
|
1906
1903
|
| `rule` | str | ✓ | CSS column-rule shorthand, e.g. '1px solid #e5e7eb'. None = no rule. |
|
|
1907
|
-
| `max_chars` | int | ✓ |
|
|
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. |
|
|
1908
1905
|
|
|
1909
1906
|
<a id="textcodestyle"></a>
|
|
1910
1907
|
## TextCodeStyle
|
|
@@ -2076,7 +2073,6 @@ Authored overlay for VariablesValueStyle. Per-value font substyle for variable c
|
|
|
2076
2073
|
| Field | Type | Optional | Description |
|
|
2077
2074
|
|-------|------|:--------:|-------------|
|
|
2078
2075
|
| `font` | [FontStyle](#fontstyle) | ✓ | Variable value font style overrides. Unset fields fall back to [`style.variables.font`](#variablesstyle). |
|
|
2079
|
-
| `numeric_variant` | enum: "normal", "tabular-nums" | ✓ | OpenType numeric feature for value displays ('normal' or 'tabular-nums'). |
|
|
2080
2076
|
|
|
2081
2077
|
<a id="variablesplaceholderstyle"></a>
|
|
2082
2078
|
## VariablesPlaceholderStyle
|
|
@@ -2339,7 +2335,6 @@ Authored overlay for ScaleContinuousStyle. Continuous-scale-only config: type, d
|
|
|
2339
2335
|
| `zero` | bool \| enum: "auto" | ✓ | Force scale zero-baseline: True/False pins it; "auto" runs the Dataface smart-zero heuristic; None passes through to Vega-Lite. |
|
|
2340
2336
|
| `type` | enum: "linear", "log", "pow", "sqrt", "symlog", "temporal" | ✓ | Scale type override. 'linear'/'log'/'pow'/'sqrt'/'symlog' pass straight through to Vega-Lite's quantitative scale.type. 'temporal' is a Dataface escape hatch for a cartesian x-axis: it forces a continuous temporal scale instead of the auto-inferred ordinal/nominal bucketed type, which is required before an authored ``domain`` of ISO dates can extend the visible range past the data extent. None lets Dataface/Vega-Lite infer from the field type. |
|
|
2341
2337
|
| `domain` | list[Any] | ✓ | Explicit [low, high] scale domain; None lets Vega-Lite auto-determine from data. Must have exactly 2 elements — Dataface only wires a continuous range override through to Vega-Lite, not an explicit category enumeration. When ``type: temporal`` is set, both elements must be ISO-8601 date/datetime strings. |
|
|
2342
|
-
| `domain_strategy` | enum: "stable", "auto" | ✓ | How the numeric domain responds to filter-variable changes when no explicit `domain` is authored. 'stable' (the default when None) pins the domain to the chart query's full unfiltered extent — the same value lands at the same pixel across variable changes. 'auto' rescales the domain to the current filtered subset on every variable change. An explicit `domain` always wins over either strategy. |
|
|
2343
2338
|
| `log` | [ScaleLogStyle](#scalelogstyle) | ✓ | Log-scale param (base); only meaningful with type: log. |
|
|
2344
2339
|
| `pow` | [ScalePowStyle](#scalepowstyle) | ✓ | Power-scale param (exponent); only meaningful with type: pow. |
|
|
2345
2340
|
| `symlog` | [ScaleSymlogStyle](#scalesymlogstyle) | ✓ | Symlog-scale param (constant); only meaningful with type: symlog. |
|
|
@@ -153,7 +153,7 @@ in queries.
|
|
|
153
153
|
dft serve
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
-
Visit a landing page (e.g. `http://localhost:
|
|
156
|
+
Visit a landing page (e.g. `http://localhost:8001/zendesk/`) and click one of
|
|
157
157
|
the data links. It should redirect to the authored face. If it serves the
|
|
158
158
|
generic system view instead, the alias is not set — check the face file.
|
|
159
159
|
|
dataface/cli/_error_format.py
CHANGED
|
@@ -15,6 +15,7 @@ not from a caller-supplied flag.
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import json
|
|
18
|
+
import sys
|
|
18
19
|
from pathlib import Path
|
|
19
20
|
|
|
20
21
|
from rich.markup import escape
|
|
@@ -50,25 +51,28 @@ def print_error(message: str) -> None:
|
|
|
50
51
|
console.print(f"[bold red]Error:[/] {message}")
|
|
51
52
|
|
|
52
53
|
|
|
54
|
+
def emit_diagnostics_jsonl(diags: list[Diagnostic]) -> None:
|
|
55
|
+
"""Write diagnostics as JSON Lines to stderr — one compact JSON object per line.
|
|
56
|
+
|
|
57
|
+
Callers use this for the ``--diagnostics-json`` path: every diagnostic
|
|
58
|
+
(error or warning) is emitted to stderr so stdout stays the render payload,
|
|
59
|
+
and line-delimited JSON survives a truncated read with whole objects intact.
|
|
60
|
+
"""
|
|
61
|
+
for d in diags:
|
|
62
|
+
print(json.dumps(d.model_dump(exclude_none=True)), file=sys.stderr)
|
|
63
|
+
|
|
64
|
+
|
|
53
65
|
def print_diagnostics(
|
|
54
66
|
diags: list[Diagnostic],
|
|
55
67
|
*,
|
|
56
|
-
json_output: bool = False,
|
|
57
68
|
path: Path | None = None,
|
|
58
69
|
) -> None:
|
|
59
|
-
"""Print Diagnostics as Rich panels
|
|
70
|
+
"""Print Diagnostics as Rich panels to stderr.
|
|
60
71
|
|
|
61
72
|
`path` is a fallback "At:" location used only when a diagnostic has no
|
|
62
73
|
`range` of its own (e.g. compile-time warnings that don't carry position
|
|
63
74
|
data yet).
|
|
64
75
|
"""
|
|
65
|
-
if json_output:
|
|
66
|
-
print(
|
|
67
|
-
json.dumps(
|
|
68
|
-
{"errors": [d.model_dump(exclude_none=True) for d in diags]}, indent=2
|
|
69
|
-
)
|
|
70
|
-
)
|
|
71
|
-
return
|
|
72
76
|
console = dft_console(stderr=True)
|
|
73
77
|
for d in diags:
|
|
74
78
|
style = "red" if d.level == "error" else "yellow"
|
dataface/cli/commands/render.py
CHANGED
|
@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING
|
|
|
10
10
|
import typer
|
|
11
11
|
|
|
12
12
|
from dataface.agent_api import Diagnostic, RenderFormat
|
|
13
|
-
from dataface.cli._error_format import print_diagnostics
|
|
13
|
+
from dataface.cli._error_format import emit_diagnostics_jsonl, print_diagnostics
|
|
14
14
|
from dataface.cli._project import with_project
|
|
15
15
|
from dataface.cli.filesystem_project import FilesystemProject
|
|
16
16
|
|
|
@@ -119,6 +119,7 @@ def _emit_result(
|
|
|
119
119
|
no_warnings: bool,
|
|
120
120
|
fail_on_chart_errors: bool,
|
|
121
121
|
print0: bool,
|
|
122
|
+
diagnostics_json: bool = False,
|
|
122
123
|
) -> str | None:
|
|
123
124
|
"""Turn a completed render into terminal/file output, or raise RenderFailed.
|
|
124
125
|
|
|
@@ -136,7 +137,11 @@ def _emit_result(
|
|
|
136
137
|
if fail_on_chart_errors and result.chart_errors:
|
|
137
138
|
raise RenderFailed(result.chart_errors)
|
|
138
139
|
|
|
139
|
-
if
|
|
140
|
+
if diagnostics_json:
|
|
141
|
+
all_diags = list(result.warnings) + list(result.chart_errors)
|
|
142
|
+
if all_diags:
|
|
143
|
+
emit_diagnostics_jsonl(all_diags)
|
|
144
|
+
elif not no_warnings:
|
|
140
145
|
_print_warnings(result.warnings)
|
|
141
146
|
|
|
142
147
|
if format in ("json", "data"):
|
|
@@ -173,6 +178,7 @@ def render_command(
|
|
|
173
178
|
max_workers: int | None = None,
|
|
174
179
|
print0: bool = False,
|
|
175
180
|
chart: str | None = None,
|
|
181
|
+
diagnostics_json: bool = False,
|
|
176
182
|
*,
|
|
177
183
|
project: FilesystemProject,
|
|
178
184
|
) -> str | None:
|
|
@@ -258,6 +264,7 @@ def render_command(
|
|
|
258
264
|
no_warnings=no_warnings,
|
|
259
265
|
fail_on_chart_errors=fail_on_chart_errors,
|
|
260
266
|
print0=print0,
|
|
267
|
+
diagnostics_json=diagnostics_json,
|
|
261
268
|
)
|
|
262
269
|
|
|
263
270
|
|
|
@@ -269,7 +276,7 @@ def render_command_from_yaml(
|
|
|
269
276
|
variables: dict[str, str] | None = None,
|
|
270
277
|
use_cache: bool = True,
|
|
271
278
|
cache_path: Path | None = None,
|
|
272
|
-
|
|
279
|
+
diagnostics_json: bool = False,
|
|
273
280
|
no_warnings: bool = False,
|
|
274
281
|
ignore_codes: set[str] | None = None,
|
|
275
282
|
fail_on_chart_errors: bool = True,
|
|
@@ -296,7 +303,8 @@ def render_command_from_yaml(
|
|
|
296
303
|
if absent), overriding project config. None (the default) falls
|
|
297
304
|
through to the project's dataface.yml cache: block (path: null
|
|
298
305
|
there means the zero-config in-memory default).
|
|
299
|
-
|
|
306
|
+
diagnostics_json: Emit all diagnostics (errors and warnings) as JSON Lines
|
|
307
|
+
to stderr. Stdout stays the render payload on success.
|
|
300
308
|
no_warnings: Suppress stderr warning output (warnings still appear in JSON)
|
|
301
309
|
ignore_codes: Warning codes to suppress via the partition seam
|
|
302
310
|
fail_on_chart_errors: Exit 1 when any per-chart runtime error exists
|
|
@@ -354,9 +362,14 @@ def render_command_from_yaml(
|
|
|
354
362
|
no_warnings=no_warnings,
|
|
355
363
|
fail_on_chart_errors=fail_on_chart_errors,
|
|
356
364
|
print0=print0,
|
|
365
|
+
diagnostics_json=diagnostics_json,
|
|
357
366
|
)
|
|
358
367
|
except RenderFailed as exc:
|
|
359
|
-
|
|
368
|
+
if exc.errors:
|
|
369
|
+
if diagnostics_json:
|
|
370
|
+
emit_diagnostics_jsonl(exc.errors)
|
|
371
|
+
else:
|
|
372
|
+
print_diagnostics(exc.errors)
|
|
360
373
|
raise typer.Exit(1) from exc
|
|
361
374
|
|
|
362
375
|
|
|
@@ -436,7 +449,7 @@ def render_commands(
|
|
|
436
449
|
variables: dict[str, str] | None = None,
|
|
437
450
|
use_cache: bool = True,
|
|
438
451
|
cache_path: Path | None = None,
|
|
439
|
-
|
|
452
|
+
diagnostics_json: bool = False,
|
|
440
453
|
no_warnings: bool = False,
|
|
441
454
|
ignore_codes: set[str] | None = None,
|
|
442
455
|
fail_on_chart_errors: bool = True,
|
|
@@ -491,9 +504,14 @@ def render_commands(
|
|
|
491
504
|
max_workers=max_workers,
|
|
492
505
|
print0=print0,
|
|
493
506
|
chart=chart,
|
|
507
|
+
diagnostics_json=diagnostics_json,
|
|
494
508
|
)
|
|
495
509
|
except RenderFailed as exc:
|
|
496
|
-
|
|
510
|
+
if exc.errors:
|
|
511
|
+
if diagnostics_json:
|
|
512
|
+
emit_diagnostics_jsonl(exc.errors)
|
|
513
|
+
else:
|
|
514
|
+
print_diagnostics(exc.errors)
|
|
497
515
|
has_failure = True
|
|
498
516
|
if fail_fast:
|
|
499
517
|
raise typer.Exit(1) from exc
|
dataface/cli/main.py
CHANGED
|
@@ -718,14 +718,14 @@ def render(
|
|
|
718
718
|
),
|
|
719
719
|
),
|
|
720
720
|
] = None,
|
|
721
|
-
|
|
721
|
+
diagnostics_json: Annotated[
|
|
722
722
|
bool,
|
|
723
723
|
typer.Option(
|
|
724
|
-
"--json
|
|
724
|
+
"--diagnostics-json",
|
|
725
725
|
help=(
|
|
726
|
-
"Emit errors as JSON to
|
|
727
|
-
"
|
|
728
|
-
"
|
|
726
|
+
"Emit all diagnostics (errors and warnings) as JSON Lines to stderr. "
|
|
727
|
+
"Stdout stays the render payload on success; stderr carries "
|
|
728
|
+
"one compact JSON object per diagnostic, line-delimited."
|
|
729
729
|
),
|
|
730
730
|
),
|
|
731
731
|
] = False,
|
|
@@ -830,7 +830,7 @@ def render(
|
|
|
830
830
|
dft render faces/sales.yml --format terminal
|
|
831
831
|
dft render faces/sales.yml --no-cache
|
|
832
832
|
dft render faces/sales.yml --cache renders.duckdb
|
|
833
|
-
dft render faces/sales.yml --json
|
|
833
|
+
dft render faces/sales.yml --diagnostics-json
|
|
834
834
|
dft render faces/sales.yml --no-warnings
|
|
835
835
|
dft render faces/sales.yml --ignore-warning WARN-REDUNDANT-ENCODING
|
|
836
836
|
dft render faces/sales.yml --allow-chart-errors
|
|
@@ -891,7 +891,7 @@ def render(
|
|
|
891
891
|
variables=variables or None,
|
|
892
892
|
use_cache=use_cache,
|
|
893
893
|
cache_path=cache,
|
|
894
|
-
|
|
894
|
+
diagnostics_json=diagnostics_json,
|
|
895
895
|
no_warnings=no_warnings,
|
|
896
896
|
ignore_codes=ignore_codes,
|
|
897
897
|
fail_on_chart_errors=fail_on_chart_errors,
|
|
@@ -909,7 +909,7 @@ def render(
|
|
|
909
909
|
variables=variables or None,
|
|
910
910
|
use_cache=use_cache,
|
|
911
911
|
cache_path=cache,
|
|
912
|
-
|
|
912
|
+
diagnostics_json=diagnostics_json,
|
|
913
913
|
no_warnings=no_warnings,
|
|
914
914
|
ignore_codes=ignore_codes,
|
|
915
915
|
fail_on_chart_errors=fail_on_chart_errors,
|
|
@@ -42,6 +42,7 @@ from dataface.core.compile.models.face.normalized import (
|
|
|
42
42
|
Layout,
|
|
43
43
|
LayoutItem,
|
|
44
44
|
)
|
|
45
|
+
from dataface.core.compile.sizing import chart_slot_width
|
|
45
46
|
|
|
46
47
|
|
|
47
48
|
def focus_on_chart(face: Face, chart_id: str) -> Face:
|
|
@@ -50,6 +51,9 @@ def focus_on_chart(face: Face, chart_id: str) -> Face:
|
|
|
50
51
|
Creates a new face with:
|
|
51
52
|
- Only the specified chart in the layout
|
|
52
53
|
- Only the variables that chart depends on
|
|
54
|
+
- The chart pinned at the slot width the dashboard layout assigned it,
|
|
55
|
+
so the focused render is geometry-identical to the dashboard (WYSIWYG);
|
|
56
|
+
a chart not placed in the layout keeps its preferred width
|
|
53
57
|
- All sources and queries unchanged (they're lazy-loaded anyway)
|
|
54
58
|
|
|
55
59
|
This is called automatically when `chart_focus` is set in the YAML.
|
|
@@ -88,8 +92,15 @@ def focus_on_chart(face: Face, chart_id: str) -> Face:
|
|
|
88
92
|
if name in chart.variable_dependencies
|
|
89
93
|
}
|
|
90
94
|
|
|
91
|
-
#
|
|
92
|
-
|
|
95
|
+
# Pin the dashboard slot width so the focused chart keeps the exact
|
|
96
|
+
# geometry it had on the board (band widths, axis-label fit). The board
|
|
97
|
+
# container hugs the pinned width the same way it hugs preferred widths.
|
|
98
|
+
slot_width = chart_slot_width(face, chart_id)
|
|
99
|
+
chart_item = LayoutItem(
|
|
100
|
+
type="chart",
|
|
101
|
+
chart=chart,
|
|
102
|
+
user_width=None if slot_width is None else f"{slot_width}px",
|
|
103
|
+
)
|
|
93
104
|
simple_layout = Layout(type=LayoutType.ROWS, items=[chart_item])
|
|
94
105
|
|
|
95
106
|
# Create new face with focused content
|
|
@@ -36,13 +36,10 @@ Usage:
|
|
|
36
36
|
)
|
|
37
37
|
"""
|
|
38
38
|
|
|
39
|
-
import logging
|
|
40
39
|
from typing import Any
|
|
41
40
|
|
|
42
41
|
import yaml
|
|
43
42
|
|
|
44
|
-
logger = logging.getLogger(__name__)
|
|
45
|
-
|
|
46
43
|
|
|
47
44
|
def update_chart_in_yaml(
|
|
48
45
|
yaml_content: str,
|
|
@@ -147,6 +144,25 @@ def update_chart_in_yaml(
|
|
|
147
144
|
if field in edits and edits[field]:
|
|
148
145
|
chart_def[field] = edits[field]
|
|
149
146
|
|
|
147
|
+
# Validate rewritability before any mutation.
|
|
148
|
+
# A rows/cols source_path must be exactly two segments (layout_key.N) where N
|
|
149
|
+
# is a digit — that is the only shape _update_layout_reference can rewrite.
|
|
150
|
+
# Deeper paths (rows.0.cols.1) name a chart inside a nested face; the nested
|
|
151
|
+
# face's own file must name the chart instead.
|
|
152
|
+
if source_path and source_path.split(".")[0] in ("rows", "cols"):
|
|
153
|
+
_segs = source_path.split(".")
|
|
154
|
+
if len(_segs) != 2:
|
|
155
|
+
raise ValueError(
|
|
156
|
+
f"Cannot promote inline chart at '{source_path}': only top-level "
|
|
157
|
+
"layout positions (rows.N / cols.N) are supported; name the chart "
|
|
158
|
+
"in the nested face's own file instead"
|
|
159
|
+
)
|
|
160
|
+
if not _segs[1].isdigit():
|
|
161
|
+
raise ValueError(
|
|
162
|
+
f"Cannot promote inline chart at '{source_path}': "
|
|
163
|
+
f"'{_segs[1]}' is not a valid integer index"
|
|
164
|
+
)
|
|
165
|
+
|
|
150
166
|
# Update the charts section
|
|
151
167
|
# If chart already exists, merge with existing values
|
|
152
168
|
if chart_id in data["charts"]:
|
|
@@ -171,31 +187,19 @@ def update_chart_in_yaml(
|
|
|
171
187
|
def _update_layout_reference(
|
|
172
188
|
data: dict[str, Any], source_path: str, chart_id: str
|
|
173
189
|
) -> None:
|
|
174
|
-
"""
|
|
190
|
+
"""Replace an inline chart at a top-level layout position with a named reference.
|
|
175
191
|
|
|
176
192
|
Args:
|
|
177
193
|
data: Parsed YAML data (modified in place)
|
|
178
|
-
source_path:
|
|
194
|
+
source_path: Validated path of the form "rows.N" or "cols.N".
|
|
195
|
+
Caller guarantees exactly two segments and a digit index.
|
|
179
196
|
chart_id: ID of the chart to reference
|
|
180
197
|
"""
|
|
181
|
-
|
|
182
|
-
# `data[layout_key]`, and a deeper path (`rows.0.cols.1`) names an entry in a
|
|
183
|
-
# nested face's own list, which this function never walks into.
|
|
184
|
-
segments = source_path.split(".")
|
|
185
|
-
if len(segments) != 2 or segments[0] not in ("rows", "cols"):
|
|
186
|
-
logger.debug(f"Unrecognized source_path format: {source_path}")
|
|
187
|
-
return
|
|
188
|
-
layout_key, raw_index = segments
|
|
189
|
-
if not raw_index.isdigit():
|
|
190
|
-
logger.warning(f"Failed to parse index from source_path: {source_path}")
|
|
191
|
-
return
|
|
198
|
+
layout_key, raw_index = source_path.split(".")
|
|
192
199
|
index = int(raw_index)
|
|
193
|
-
|
|
194
|
-
# Update the layout
|
|
195
200
|
if (
|
|
196
201
|
layout_key in data
|
|
197
202
|
and isinstance(data[layout_key], list)
|
|
198
203
|
and 0 <= index < len(data[layout_key])
|
|
199
204
|
):
|
|
200
|
-
# Replace inline chart with named reference
|
|
201
205
|
data[layout_key][index] = chart_id
|
dataface/core/compile/jinja.py
CHANGED
|
@@ -48,6 +48,7 @@ from jinja2 import (
|
|
|
48
48
|
|
|
49
49
|
from dataface.core.compile._jinja_helpers import _LenientUndefined, _QueryNamespace
|
|
50
50
|
from dataface.core.compile.errors import CompilationError, JinjaError
|
|
51
|
+
from dataface.core.compile.parameterized import _LiteralCollector, _ParameterizedValue
|
|
51
52
|
|
|
52
53
|
logger = logging.getLogger(__name__)
|
|
53
54
|
|
|
@@ -554,10 +555,25 @@ def resolve_query_sql_with_dependencies(
|
|
|
554
555
|
# Phase 2: resolve variables via Jinja. filter()/filter_date_range() calls
|
|
555
556
|
# are deferred (left verbatim) so they reach the SQL adapter's parameterized
|
|
556
557
|
# pass instead of the raising compile-time stub — see resolve_jinja_template.
|
|
558
|
+
#
|
|
559
|
+
# Variables are wrapped in _ParameterizedValue with a _LiteralCollector so
|
|
560
|
+
# that {{ n }} still inlines as a literal (the reference-path contract) while
|
|
561
|
+
# {{ n | int }} / {{ n | float }} raise — matching the direct-path behavior
|
|
562
|
+
# and preventing silent value changes when {{ queries.X }} refs are added or
|
|
563
|
+
# removed. Using the same wrapper class as the direct path ensures equality,
|
|
564
|
+
# membership, and iteration operators delegate correctly to the raw value.
|
|
557
565
|
if target_sql and ("{{" in target_sql or "{%" in target_sql):
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
566
|
+
_collector = _LiteralCollector()
|
|
567
|
+
guarded_vars = {
|
|
568
|
+
name: _ParameterizedValue(name, val, _collector)
|
|
569
|
+
for name, val in (variables.items() if variables else ())
|
|
570
|
+
}
|
|
571
|
+
try:
|
|
572
|
+
target_sql = resolve_jinja_template(
|
|
573
|
+
target_sql, variables=guarded_vars, defer_filters=True
|
|
574
|
+
)
|
|
575
|
+
except (TypeError, KeyError) as e:
|
|
576
|
+
raise JinjaError(f"Template error: {e}", target_sql) from e
|
|
561
577
|
|
|
562
578
|
return target_sql
|
|
563
579
|
|
|
@@ -8,10 +8,11 @@ leaf move creates its destination container on demand, and the later
|
|
|
8
8
|
container move must find that destination still empty (or absent) to avoid
|
|
9
9
|
a spurious MigrationConflictError against an unrelated field that also sets
|
|
10
10
|
the old container. See dataface/AGENTS.md's "Two validation boundaries" —
|
|
11
|
-
these are structural moves; categorical_orient (deleted),
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
raise UnsupportedSchemaError for
|
|
11
|
+
these are structural moves; categorical_orient (deleted), the stable-axis
|
|
12
|
+
domain-strategy key (deleted), ticks.interval (reinterpreted vocabulary, not
|
|
13
|
+
renamed), and the band/point/quantize scale groups (deleted) are semantic
|
|
14
|
+
changes with no lossless move and are left to raise UnsupportedSchemaError for
|
|
15
|
+
manual migration.
|
|
15
16
|
"""
|
|
16
17
|
|
|
17
18
|
from __future__ import annotations
|
|
@@ -29,7 +30,6 @@ RENAMES: tuple[tuple[YamlKeyPath, YamlKeyPath], ...] = (
|
|
|
29
30
|
(("scale", "continuous", "domain"), ("scale", "domain")),
|
|
30
31
|
(("scale", "continuous", "zero"), ("scale", "zero")),
|
|
31
32
|
(("scale", "continuous", "type"), ("scale", "type")),
|
|
32
|
-
(("scale", "continuous", "domain_strategy"), ("scale", "domain_strategy")),
|
|
33
33
|
(("scale", "continuous", "log", "base"), ("scale", "base")),
|
|
34
34
|
(("scale", "continuous", "pow", "exponent"), ("scale", "exponent")),
|
|
35
35
|
(("scale", "continuous", "symlog", "constant"), ("scale", "constant")),
|
|
@@ -64,7 +64,6 @@ from ._conditional_formatting import (
|
|
|
64
64
|
ConditionalRule,
|
|
65
65
|
FieldConditionalFormatting,
|
|
66
66
|
_PredicateBase,
|
|
67
|
-
coerce_numeric,
|
|
68
67
|
match_predicate,
|
|
69
68
|
)
|
|
70
69
|
from ._data_table import (
|
|
@@ -211,7 +210,6 @@ __all__ = [
|
|
|
211
210
|
"FieldConditionalFormatting",
|
|
212
211
|
"_PREDICATE_OPS",
|
|
213
212
|
"_PredicateBase",
|
|
214
|
-
"coerce_numeric",
|
|
215
213
|
"match_predicate",
|
|
216
214
|
# data_table
|
|
217
215
|
"CHART_DATA_TABLE_SUPPORTED_TYPES",
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import math
|
|
6
5
|
from typing import Any, Literal
|
|
7
6
|
|
|
8
7
|
from pydantic import (
|
|
@@ -20,6 +19,7 @@ from dataface.core.compile.models.style.theme import (
|
|
|
20
19
|
VALID_FONT_WEIGHTS,
|
|
21
20
|
font_weight_as_css,
|
|
22
21
|
)
|
|
22
|
+
from dataface.core.utils import coerce_numeric_cell
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
class _PredicateBase(BaseModel):
|
|
@@ -120,22 +120,6 @@ def _validate_exactly_one_predicate(obj: Any) -> None:
|
|
|
120
120
|
)
|
|
121
121
|
|
|
122
122
|
|
|
123
|
-
def coerce_numeric(v: Any) -> float | None:
|
|
124
|
-
"""Return float(v) for finite numeric-compatible values; None otherwise."""
|
|
125
|
-
if v is None or isinstance(v, bool):
|
|
126
|
-
return None
|
|
127
|
-
if isinstance(v, (int, float)):
|
|
128
|
-
result = float(v)
|
|
129
|
-
return result if math.isfinite(result) else None
|
|
130
|
-
if isinstance(v, str):
|
|
131
|
-
try:
|
|
132
|
-
result = float(v)
|
|
133
|
-
return result if math.isfinite(result) else None
|
|
134
|
-
except (ValueError, TypeError):
|
|
135
|
-
return None
|
|
136
|
-
return None
|
|
137
|
-
|
|
138
|
-
|
|
139
123
|
def _in_value_matches(candidate: Any, value: Any) -> bool:
|
|
140
124
|
"""Equality check for a single ``in`` candidate, with the bool/int guard."""
|
|
141
125
|
if isinstance(value, bool) != isinstance(candidate, bool):
|
|
@@ -159,7 +143,7 @@ def match_predicate(rule: _PredicateBase, value: Any) -> bool:
|
|
|
159
143
|
return value != rule.ne
|
|
160
144
|
if rule.in_ is not None:
|
|
161
145
|
return any(_in_value_matches(candidate, value) for candidate in rule.in_)
|
|
162
|
-
v_coerced =
|
|
146
|
+
v_coerced = coerce_numeric_cell(value)
|
|
163
147
|
if v_coerced is None:
|
|
164
148
|
return False
|
|
165
149
|
v = v_coerced
|
|
@@ -162,6 +162,22 @@ 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
|
+
|
|
165
181
|
class DataTableConfig(ConfigNode):
|
|
166
182
|
divider_gap: float
|
|
167
183
|
chart_data_table_max_x_ticks: int
|
|
@@ -174,6 +190,7 @@ class ChartRenderingConfig(ConfigNode):
|
|
|
174
190
|
bar: BarConfig
|
|
175
191
|
type_inference: TypeInferenceConfig
|
|
176
192
|
board: BoardConfig
|
|
193
|
+
prose: ProseConfig
|
|
177
194
|
data_table: DataTableConfig
|
|
178
195
|
stroke: StrokeConfig
|
|
179
196
|
axis: AxisConfig
|
|
@@ -201,7 +201,6 @@ class ResolvedScaleContinuousStyle(BaseModel):
|
|
|
201
201
|
zero: bool | Literal["auto"] | None = None
|
|
202
202
|
type: Literal["linear", "log", "pow", "sqrt", "symlog", "temporal"] | None = None
|
|
203
203
|
domain: list[Any] | None = None
|
|
204
|
-
domain_strategy: Literal["stable", "auto"] | None = None
|
|
205
204
|
log: ResolvedScaleLogStyle | None = None
|
|
206
205
|
pow: ResolvedScalePowStyle | None = None
|
|
207
206
|
symlog: ResolvedScaleSymlogStyle | None = None
|
|
@@ -572,7 +572,6 @@ def build_resolved_scale(
|
|
|
572
572
|
zero=scale.continuous.zero,
|
|
573
573
|
type=scale.continuous.type,
|
|
574
574
|
domain=scale.continuous.domain,
|
|
575
|
-
domain_strategy=scale.continuous.domain_strategy,
|
|
576
575
|
log=(
|
|
577
576
|
ResolvedScaleLogStyle(base=scale.continuous.log.base)
|
|
578
577
|
if scale.continuous.log is not None
|
|
@@ -104,7 +104,6 @@ from dataface.core.compile.models.style.theme.board import (
|
|
|
104
104
|
TitleWidthOffsetsStyle,
|
|
105
105
|
_normalize_overflow_value,
|
|
106
106
|
coerce_gap,
|
|
107
|
-
compute_text_column_count,
|
|
108
107
|
font_weight_as_css,
|
|
109
108
|
)
|
|
110
109
|
from dataface.core.compile.models.style.theme.callout import (
|
|
@@ -414,6 +413,5 @@ __all__ = [
|
|
|
414
413
|
"_RadialChartStyle",
|
|
415
414
|
"_normalize_overflow_value",
|
|
416
415
|
"coerce_gap",
|
|
417
|
-
"compute_text_column_count",
|
|
418
416
|
"font_weight_as_css",
|
|
419
417
|
]
|
|
@@ -585,17 +585,6 @@ class ScaleContinuousStyle(ScaleDomainValidationMixin):
|
|
|
585
585
|
"date/datetime strings."
|
|
586
586
|
),
|
|
587
587
|
)
|
|
588
|
-
domain_strategy: Literal["stable", "auto"] | None = Field(
|
|
589
|
-
default=None,
|
|
590
|
-
description=(
|
|
591
|
-
"How the numeric domain responds to filter-variable changes when no "
|
|
592
|
-
"explicit `domain` is authored. 'stable' (the default when None) pins "
|
|
593
|
-
"the domain to the chart query's full unfiltered extent — the same "
|
|
594
|
-
"value lands at the same pixel across variable changes. 'auto' "
|
|
595
|
-
"rescales the domain to the current filtered subset on every variable "
|
|
596
|
-
"change. An explicit `domain` always wins over either strategy."
|
|
597
|
-
),
|
|
598
|
-
)
|
|
599
588
|
log: ScaleLogStyle | None = Field(
|
|
600
589
|
default=None,
|
|
601
590
|
description="Log-scale param (base); only meaningful with type: log.",
|