dataface 0.1.6.dev360__py3-none-any.whl → 0.1.6.dev476__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 +26 -0
- dataface/agent_api/dashboards.py +1 -1
- dataface/agent_api/describe.py +10 -8
- dataface/agent_api/docs/yaml-reference.md +33 -0
- dataface/agent_api/project_session.py +6 -6
- dataface/agent_api/query.py +2 -2
- dataface/agent_api/render_face.py +12 -4
- dataface/agent_api/validate.py +10 -8
- dataface/ai/agent.py +1 -1
- dataface/ai/skills/dashboard-build/SKILL.md +2 -0
- dataface/core/compile/__init__.py +1 -1
- dataface/core/compile/channel.py +10 -3
- dataface/core/compile/compiler.py +22 -25
- dataface/core/compile/data_table_attachment.py +22 -15
- dataface/core/compile/detect.py +21 -37
- dataface/core/compile/models/chart/authored.py +11 -0
- dataface/core/compile/models/chart/normalized.py +7 -0
- dataface/core/compile/models/style/resolved.py +2 -0
- dataface/core/compile/models/style/theme.py +127 -2
- dataface/core/compile/style_cascade.py +4 -0
- dataface/core/dashboard.py +8 -5
- dataface/core/defaults/themes/_base.yaml +5 -0
- dataface/core/defaults/themes/stark.yaml +12 -0
- dataface/core/execute/_duckdb_cache_base.py +272 -0
- dataface/core/execute/adapters/schema_adapter.py +83 -0
- dataface/core/execute/batch.py +1 -2
- dataface/core/execute/cache_backend.py +1 -35
- dataface/core/execute/duckdb_cache.py +25 -333
- dataface/core/execute/executor.py +0 -158
- dataface/core/execute/parallel.py +7 -114
- dataface/core/execute/trivial_local_cache.py +183 -0
- dataface/core/inspect/templates/categorical_column.yml +1 -1
- dataface/core/inspect/templates/date_column.yml +1 -1
- dataface/core/inspect/templates/model.yml +2 -2
- dataface/core/inspect/templates/numeric_column.yml +1 -1
- dataface/core/inspect/templates/quality.yml +1 -1
- dataface/core/inspect/templates/string_column.yml +1 -1
- dataface/core/project.py +61 -2
- dataface/core/registered_views/link_keys.py +17 -14
- dataface/core/registered_views/render_pipeline.py +16 -10
- dataface/core/registered_views/variable_planner.py +19 -19
- dataface/core/render/chart/auto_link.py +349 -72
- dataface/core/render/chart/decisions.py +18 -10
- dataface/core/render/chart/render_single.py +292 -23
- dataface/core/render/chart/standard_renderer.py +220 -9
- dataface/core/render/chart/table.py +123 -17
- dataface/core/render/chart/table_support.py +49 -4
- dataface/core/render/chart/vega_lite_types.py +17 -0
- dataface/core/render/chart/vl_field_maps.py +2 -0
- dataface/core/render/chrome_css.py +56 -0
- dataface/core/render/face_api.py +2 -2
- dataface/core/render/renderer.py +37 -33
- dataface/core/render/templates/controls/_styles.css +60 -62
- dataface/core/render/templates/nav/nav-fragment.html +1 -0
- dataface/core/render/templates/nav/nav.css +18 -19
- dataface/core/render/templates/scripts/variables.js +4 -4
- dataface/core/render/templates/variable_controls/container.html +2 -8
- dataface/core/render/text/case.py +79 -8
- dataface/core/render/variable_controls.py +2 -9
- dataface/core/render/warnings/base.py +17 -0
- dataface/core/render/warnings/pie_dominant_segment.py +68 -0
- dataface/core/render/warnings/pie_too_many_segments.py +47 -0
- dataface/core/render/warnings/registry.py +8 -0
- dataface/core/render/warnings/too_many_color_categories.py +68 -0
- dataface/core/render/warnings/too_many_x_categories.py +63 -0
- dataface/core/serve/alias_index.py +55 -6
- dataface/core/serve/server.py +33 -25
- {dataface-0.1.6.dev360.dist-info → dataface-0.1.6.dev476.dist-info}/METADATA +1 -1
- {dataface-0.1.6.dev360.dist-info → dataface-0.1.6.dev476.dist-info}/RECORD +72 -65
- {dataface-0.1.6.dev360.dist-info → dataface-0.1.6.dev476.dist-info}/WHEEL +0 -0
- {dataface-0.1.6.dev360.dist-info → dataface-0.1.6.dev476.dist-info}/entry_points.txt +0 -0
- {dataface-0.1.6.dev360.dist-info → dataface-0.1.6.dev476.dist-info}/licenses/LICENSE +0 -0
dataface/DATAFACE_SYNTAX.md
CHANGED
|
@@ -253,6 +253,32 @@ Rules:
|
|
|
253
253
|
- Each alias must be unique across the project — two faces claiming the same alias is a startup error.
|
|
254
254
|
- Aliasing a generated system route (data or inspector view) is allowed: the redirect takes precedence, letting you override what that URL serves.
|
|
255
255
|
|
|
256
|
+
#### Parameterized aliases (capture a path segment into a variable)
|
|
257
|
+
|
|
258
|
+
An alias may contain `<name>` capture segments. A request matching the pattern
|
|
259
|
+
302-redirects to the face's canonical URL with each captured segment appended as a
|
|
260
|
+
query param of the same name — which the face then reads as its variable. This
|
|
261
|
+
gives a single detail face a clean per-entity URL:
|
|
262
|
+
|
|
263
|
+
```yaml
|
|
264
|
+
# faces/milestone.yml (canonical URL: /milestone)
|
|
265
|
+
aliases:
|
|
266
|
+
- /milestones/<name>
|
|
267
|
+
variables:
|
|
268
|
+
name: { input: select, options: { query: milestone_options, column: slug } }
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Now `/milestones/m3-public-launch` redirects to `/milestone/?name=m3-public-launch`,
|
|
272
|
+
and `milestone.yml` renders with `name` bound to `m3-public-launch`.
|
|
273
|
+
|
|
274
|
+
- Capture names use the same grammar as built-in routes: `<name>` matches exactly
|
|
275
|
+
one non-empty path segment (no `/`). Use a capture name that matches the
|
|
276
|
+
variable you want it to fill.
|
|
277
|
+
- A real face file always wins over a pattern alias, so `/milestones/` (the list
|
|
278
|
+
face) and `/milestones/<name>` (the detail redirect) coexist without conflict.
|
|
279
|
+
- Plain aliases (no `<...>`) redirect as before; only aliases with a capture are
|
|
280
|
+
treated as patterns.
|
|
281
|
+
|
|
256
282
|
There are two ways to override a data/inspector route for a given path:
|
|
257
283
|
- **Face file at that path** — create `faces/data/warehouse/schema/table.yml` and the server renders it directly (no redirect).
|
|
258
284
|
- **`aliases:` entry** — any face can declare `/data/…/` as an alias; the server issues a 302 to the declaring face's canonical URL.
|
dataface/agent_api/dashboards.py
CHANGED
|
@@ -244,7 +244,7 @@ def get_dashboard(
|
|
|
244
244
|
],
|
|
245
245
|
)
|
|
246
246
|
|
|
247
|
-
result = compile_file(file_path, project=project)
|
|
247
|
+
result = compile_file(project.file_for_path(file_path), project=project)
|
|
248
248
|
return CompiledDashboard(
|
|
249
249
|
success=result.success,
|
|
250
250
|
dashboard=result.face if result.success else None,
|
dataface/agent_api/describe.py
CHANGED
|
@@ -224,7 +224,7 @@ def describe_face(path: Path, *, project: Project) -> DescribeFaceResult:
|
|
|
224
224
|
)
|
|
225
225
|
|
|
226
226
|
try:
|
|
227
|
-
result = compile_file(resolved, project=project)
|
|
227
|
+
result = compile_file(project.file_for_path(resolved), project=project)
|
|
228
228
|
except OSError as exc:
|
|
229
229
|
return DescribeFaceResult(
|
|
230
230
|
success=False,
|
|
@@ -350,13 +350,15 @@ def _describe_one_path(
|
|
|
350
350
|
if not resolved.is_dir():
|
|
351
351
|
return [describe_face(resolved, project=project)]
|
|
352
352
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
for
|
|
357
|
-
if
|
|
353
|
+
under = str(resolved.relative_to(project.root))
|
|
354
|
+
faces = sorted(
|
|
355
|
+
project.root / pf.relpath
|
|
356
|
+
for pf in project.iter_faces(under=under, recursive=True)
|
|
357
|
+
if pf.is_yaml
|
|
358
|
+
and not pf.is_private
|
|
359
|
+
and not pf.sibling(INSPECT_TEMPLATE_MANIFEST).exists()
|
|
358
360
|
)
|
|
359
|
-
if not
|
|
361
|
+
if not faces:
|
|
360
362
|
return [
|
|
361
363
|
DescribeFaceResult(
|
|
362
364
|
success=False,
|
|
@@ -369,4 +371,4 @@ def _describe_one_path(
|
|
|
369
371
|
],
|
|
370
372
|
)
|
|
371
373
|
]
|
|
372
|
-
return [describe_face(f, project=project) for f in
|
|
374
|
+
return [describe_face(f, project=project) for f in faces]
|
|
@@ -287,6 +287,7 @@ Authored patch for KPI (key performance indicator) charts.
|
|
|
287
287
|
| `label` | str | ✓ | KPI label rendered above the headline value. |
|
|
288
288
|
| `support` | [KpiSupportConfig](#kpisupportconfig) | ✓ | Optional support line beneath the KPI value. |
|
|
289
289
|
| `style` | [KpiChartStyle](#kpichartstyle) | ✓ | Chart-local style overrides. |
|
|
290
|
+
| `background` | str \| dict[str, Any] | ✓ | Gradient background channel — {column, scale} shape. Paints the card background by the value's position in the scale. See channel.py parse_style_channel for the full grammar. |
|
|
290
291
|
|
|
291
292
|
<a id="tablechart"></a>
|
|
292
293
|
## TableChart
|
|
@@ -848,6 +849,7 @@ Authored overlay for TableChartStyle. Table chart style overrides layered on top
|
|
|
848
849
|
| `more_rows` | [TableEdgeStyle](#tableedgestyle) | ✓ | Style for the 'more rows' edge-case indicator. |
|
|
849
850
|
| `empty_state` | [TableEdgeStyle](#tableedgestyle) | ✓ | Style for the empty-state (no data) indicator. |
|
|
850
851
|
| `spark` | [SparkStyle](#sparkstyle) | ✓ | Inline sparkline defaults for table cells. |
|
|
852
|
+
| `transpose` | bool | ✓ | When True, render a single wide data row as N (label, value) rows — one per column. Raises ChartDataError when data has more than one row. Used for Looker single-value summary tiles with multiple measures. |
|
|
851
853
|
| `text_baseline_offset` | float | ✓ | Vertical offset to align SVG text baseline with cell grid in pixels. |
|
|
852
854
|
| `title_baseline_offset` | float | ✓ | Vertical offset to align SVG title baseline in pixels. |
|
|
853
855
|
| `title_subtitle_gap` | float | ✓ | Gap between table title and subtitle lines in pixels. |
|
|
@@ -1348,6 +1350,7 @@ Authored overlay for LegendStyle.
|
|
|
1348
1350
|
| `title` | [LegendElementStyle](#legendelementstyle) | ✓ | Legend title style. |
|
|
1349
1351
|
| `visible` | bool | ✓ | Show the legend. None = legend visible; False = explicitly suppressed. |
|
|
1350
1352
|
| `interactive_legend` | bool | ✓ | Emit a dft_legend point-selection param for single-click series toggle. |
|
|
1353
|
+
| `symbol_limit` | int | ✓ | Maximum number of legend entries to display; maps to VL symbolLimit. None uses Vega-Lite's default (no cap). Set to a positive integer to prevent legend overflow on high-cardinality series. |
|
|
1351
1354
|
|
|
1352
1355
|
<a id="tooltipstyle"></a>
|
|
1353
1356
|
## TooltipStyle
|
|
@@ -1569,6 +1572,7 @@ Authored overlay for TableColumnsStyle.
|
|
|
1569
1572
|
| `default_width` | float | ✓ | Default column width in pixels. |
|
|
1570
1573
|
| `cell_padding` | float | ✓ | Horizontal padding inside table cells in pixels. |
|
|
1571
1574
|
| `width_similarity_threshold` | float | ✓ | Auto-width columns whose min/max ratio >= this threshold are snapped to a shared width before budget allocation. 1.0 disables clustering; 0.0 forces all auto-columns to equal width. |
|
|
1575
|
+
| `content_headroom` | float | ✓ | Fractional breathing room added above the raw p95 column demand when pinning compact columns in mixed (compact + text) tables. 0.10 means each compact column is pinned at 10 % above its measured demand; the extra width is funded by the text-column budget. Has no effect on all-compact tables (proportional scaling already fills the budget). 0.0 disables headroom. |
|
|
1572
1576
|
|
|
1573
1577
|
<a id="tableheaderstyle"></a>
|
|
1574
1578
|
## TableHeaderStyle
|
|
@@ -2209,6 +2213,7 @@ Authored overlay for BarMarkStyle. Bar mark geometry and stroke. Chart-level bar
|
|
|
2209
2213
|
| `padding` | float | ✓ | Padding around bar marks in pixels. |
|
|
2210
2214
|
| `size` | float | ✓ | Bar width in pixels (fixed-width mode). |
|
|
2211
2215
|
| `band_width` | float | ✓ | Bar width as a fraction of the band step (0–1). |
|
|
2216
|
+
| `labels` | [BarLabelsStyle](#barlabelsstyle) | ✓ | Value label style for bar marks. |
|
|
2212
2217
|
|
|
2213
2218
|
<a id="textmarkstyle"></a>
|
|
2214
2219
|
## TextMarkStyle
|
|
@@ -2228,6 +2233,7 @@ Authored overlay for LineMarkStyle. Line mark stroke, interpolation, and halo. P
|
|
|
2228
2233
|
| `stroke` | [StrokeStyle](#strokestyle) | ✓ | Line stroke style. |
|
|
2229
2234
|
| `curve` | str | ✓ | Line interpolation curve (e.g. 'linear', 'monotone'). |
|
|
2230
2235
|
| `halo_multiplier` | float | ✓ | Halo stroke width multiplier relative to stroke.width; 0 disables the halo. |
|
|
2236
|
+
| `labels` | [PointLabelsStyle](#pointlabelsstyle) | ✓ | Value label style for line marks. |
|
|
2231
2237
|
|
|
2232
2238
|
<a id="pointmarkstyle"></a>
|
|
2233
2239
|
## PointMarkStyle
|
|
@@ -2242,6 +2248,7 @@ Authored overlay for PointMarkStyle. Point mark style (data-point markers on lin
|
|
|
2242
2248
|
| `filled` | bool | ✓ | Whether points are filled; None uses VL default. |
|
|
2243
2249
|
| `fill` | str | ✓ | Point interior fill color; only applied when filled=false. |
|
|
2244
2250
|
| `stroke_width` | float | ✓ | Stroke width in pixels for hollow point rings; None uses VL default. |
|
|
2251
|
+
| `labels` | [PointLabelsStyle](#pointlabelsstyle) | ✓ | Value label style for point marks. |
|
|
2245
2252
|
|
|
2246
2253
|
<a id="rulemarkstyle"></a>
|
|
2247
2254
|
## RuleMarkStyle
|
|
@@ -2522,6 +2529,19 @@ Authored overlay for DataTableRowPaddingStyle.
|
|
|
2522
2529
|
| `vertical` | float | ✓ | Vertical (top/bottom) padding inside data_table rows in pixels. |
|
|
2523
2530
|
| `horizontal` | float | ✓ | Horizontal (left/right) padding inside data_table rows in pixels. |
|
|
2524
2531
|
|
|
2532
|
+
<a id="barlabelsstyle"></a>
|
|
2533
|
+
## BarLabelsStyle
|
|
2534
|
+
Authored overlay for BarLabelsStyle. Bar mark value-label config. Extends MarkLabelsStyle with bar-specific positions.
|
|
2535
|
+
|
|
2536
|
+
| Field | Type | Optional | Description |
|
|
2537
|
+
|-------|------|:--------:|-------------|
|
|
2538
|
+
| `visible` | bool | ✓ | Show numeric value labels on each mark; False by default. |
|
|
2539
|
+
| `format` | str | ✓ | Number format string for value labels. None inherits from style.charts.axis_quantitative.format at render time (the same format string used for measure-axis tick labels, e.g. '~s'). |
|
|
2540
|
+
| `dx` | int | ✓ | Horizontal pixel offset for value labels; overrides the position default. |
|
|
2541
|
+
| `dy` | int | ✓ | Vertical pixel offset for value labels; overrides the position default. |
|
|
2542
|
+
| `font` | [FontStyle](#fontstyle) | ✓ | Value label font style (color, size, family, etc.). |
|
|
2543
|
+
| `position` | enum: "top", "inside_top", "middle" | ✓ | Label position relative to the bar. 'top' places labels above the bar top (outside). 'inside_top' places labels just inside the top edge. 'middle' centers labels vertically in the bar. |
|
|
2544
|
+
|
|
2525
2545
|
<a id="strokestyle"></a>
|
|
2526
2546
|
## StrokeStyle
|
|
2527
2547
|
Authored overlay for StrokeStyle. Stroke appearance sub-block shared across mark families.
|
|
@@ -2534,6 +2554,19 @@ Authored overlay for StrokeStyle. Stroke appearance sub-block shared across mark
|
|
|
2534
2554
|
| `join` | enum: "miter", "round", "bevel" | ✓ | Stroke line join style (miter, round, or bevel). |
|
|
2535
2555
|
| `dasharray` | str | ✓ | SVG stroke-dasharray pattern (e.g. '4 2'). |
|
|
2536
2556
|
|
|
2557
|
+
<a id="pointlabelsstyle"></a>
|
|
2558
|
+
## PointLabelsStyle
|
|
2559
|
+
Authored overlay for PointLabelsStyle. Point/line mark value-label config. Used by both LineMarkStyle and PointMarkStyle.
|
|
2560
|
+
|
|
2561
|
+
| Field | Type | Optional | Description |
|
|
2562
|
+
|-------|------|:--------:|-------------|
|
|
2563
|
+
| `visible` | bool | ✓ | Show numeric value labels on each mark; False by default. |
|
|
2564
|
+
| `format` | str | ✓ | Number format string for value labels. None inherits from style.charts.axis_quantitative.format at render time (the same format string used for measure-axis tick labels, e.g. '~s'). |
|
|
2565
|
+
| `dx` | int | ✓ | Horizontal pixel offset for value labels; overrides the position default. |
|
|
2566
|
+
| `dy` | int | ✓ | Vertical pixel offset for value labels; overrides the position default. |
|
|
2567
|
+
| `font` | [FontStyle](#fontstyle) | ✓ | Value label font style (color, size, family, etc.). |
|
|
2568
|
+
| `position` | enum: "top", "bottom", "left", "right", "middle" | ✓ | Label position relative to the point. 'top' places labels above the point. 'bottom' places labels below. 'left' to the left. 'right' to the right. 'middle' centers on the point. |
|
|
2569
|
+
|
|
2537
2570
|
<a id="fontcolorstrokestyle"></a>
|
|
2538
2571
|
## FontColorStrokeStyle
|
|
2539
2572
|
Authored overlay for FontColorStrokeStyle. Stroke for tick/rule marks whose color defaults to the root font color.
|
|
@@ -35,7 +35,7 @@ from dataface.core.execute.adapters.adapter_registry import (
|
|
|
35
35
|
AdapterRegistry,
|
|
36
36
|
build_adapter_registry,
|
|
37
37
|
)
|
|
38
|
-
from dataface.core.execute.
|
|
38
|
+
from dataface.core.execute.cache_backend import QueryResultCache
|
|
39
39
|
from dataface.core.execute.observability import WarehouseObserver
|
|
40
40
|
from dataface.core.inspect.query_validator import (
|
|
41
41
|
QueryDiagnostic,
|
|
@@ -68,7 +68,7 @@ class ProjectSession:
|
|
|
68
68
|
"""
|
|
69
69
|
|
|
70
70
|
project: Project
|
|
71
|
-
cache:
|
|
71
|
+
cache: QueryResultCache | None
|
|
72
72
|
_owns_registry: bool
|
|
73
73
|
_read_only: bool
|
|
74
74
|
_connection_string: str | None
|
|
@@ -82,7 +82,7 @@ class ProjectSession:
|
|
|
82
82
|
def __init__(
|
|
83
83
|
self,
|
|
84
84
|
project: Project,
|
|
85
|
-
cache:
|
|
85
|
+
cache: QueryResultCache | None = None,
|
|
86
86
|
adapter_registry: AdapterRegistry | None = None,
|
|
87
87
|
read_only: bool = True,
|
|
88
88
|
observers: list[WarehouseObserver] | None = None,
|
|
@@ -110,7 +110,7 @@ class ProjectSession:
|
|
|
110
110
|
cls,
|
|
111
111
|
project_dir: Path | str,
|
|
112
112
|
*,
|
|
113
|
-
cache:
|
|
113
|
+
cache: QueryResultCache | None = None,
|
|
114
114
|
read_only: bool = True,
|
|
115
115
|
connection_string: str | None = None,
|
|
116
116
|
dialect: str = "duckdb",
|
|
@@ -150,7 +150,7 @@ class ProjectSession:
|
|
|
150
150
|
cls,
|
|
151
151
|
project: Project,
|
|
152
152
|
*,
|
|
153
|
-
cache:
|
|
153
|
+
cache: QueryResultCache | None = None,
|
|
154
154
|
read_only: bool = True,
|
|
155
155
|
connection_string: str | None = None,
|
|
156
156
|
dialect: str = "duckdb",
|
|
@@ -183,7 +183,7 @@ class ProjectSession:
|
|
|
183
183
|
face_file: Path | str,
|
|
184
184
|
*,
|
|
185
185
|
read_only: bool = True,
|
|
186
|
-
cache:
|
|
186
|
+
cache: QueryResultCache | None = None,
|
|
187
187
|
) -> Self:
|
|
188
188
|
"""Open a ProjectSession rooted at the project directory discovered upward from face_file."""
|
|
189
189
|
resolved = Path(face_file).resolve()
|
dataface/agent_api/query.py
CHANGED
|
@@ -47,7 +47,7 @@ def lookup_face_query_sql(
|
|
|
47
47
|
success=False, errors=[f"File not found: {file_path}"]
|
|
48
48
|
)
|
|
49
49
|
|
|
50
|
-
compile_result = compile_file(file_path, project=project)
|
|
50
|
+
compile_result = compile_file(project.file_for_path(file_path), project=project)
|
|
51
51
|
if not compile_result.success:
|
|
52
52
|
return FaceQueryLookupResult(
|
|
53
53
|
success=False, errors=[e.message for e in compile_result.errors]
|
|
@@ -275,7 +275,7 @@ def query_face(
|
|
|
275
275
|
hint = no_project_hint(project.root)
|
|
276
276
|
return _fail(name, file_path, [f"File not found: {file_path}{hint}"])
|
|
277
277
|
|
|
278
|
-
compile_result = compile_file(file_path, project=project)
|
|
278
|
+
compile_result = compile_file(project.file_for_path(file_path), project=project)
|
|
279
279
|
if not compile_result.success:
|
|
280
280
|
return _fail(name, file_path, [e.message for e in compile_result.errors])
|
|
281
281
|
|
|
@@ -13,8 +13,8 @@ from dataface.core.compile.markdown import (
|
|
|
13
13
|
is_markdown_face,
|
|
14
14
|
markdown_to_yaml,
|
|
15
15
|
)
|
|
16
|
-
from dataface.core.execute.
|
|
17
|
-
from dataface.core.project import Project
|
|
16
|
+
from dataface.core.execute.cache_backend import QueryResultCache
|
|
17
|
+
from dataface.core.project import Project, ProjectFile
|
|
18
18
|
from dataface.core.render.face_api import compile_and_render
|
|
19
19
|
|
|
20
20
|
|
|
@@ -26,7 +26,7 @@ def render_face(
|
|
|
26
26
|
project: Project | None = None,
|
|
27
27
|
variables: dict[str, str] | None = None,
|
|
28
28
|
use_cache: bool = True,
|
|
29
|
-
cache:
|
|
29
|
+
cache: QueryResultCache | None = None,
|
|
30
30
|
**options: Any,
|
|
31
31
|
) -> str | bytes:
|
|
32
32
|
"""Render a Dataface face file (.yml or .md) to embeddable output.
|
|
@@ -70,7 +70,15 @@ def render_face(
|
|
|
70
70
|
project_session = ProjectSession.from_face(face_file, cache=cache)
|
|
71
71
|
try:
|
|
72
72
|
p = project_session.project
|
|
73
|
-
|
|
73
|
+
# The face may live outside an explicitly-supplied project root — e.g.
|
|
74
|
+
# the looker_migrate eval renders faces under evals/runs/ against a
|
|
75
|
+
# project rooted at runtime/ for its sources/schema. Such a face has no
|
|
76
|
+
# project-relative handle; nested sub-file refs are unsupported in that
|
|
77
|
+
# mode (relative reads still resolve via face_dir below).
|
|
78
|
+
try:
|
|
79
|
+
base_file: ProjectFile | None = p.file_for_path(face_file)
|
|
80
|
+
except ValueError:
|
|
81
|
+
base_file = None
|
|
74
82
|
return compile_and_render(
|
|
75
83
|
yaml_content,
|
|
76
84
|
p,
|
dataface/agent_api/validate.py
CHANGED
|
@@ -110,13 +110,15 @@ def _validate_one_path(
|
|
|
110
110
|
if not resolved.is_dir():
|
|
111
111
|
return [validate(resolved, project=project)]
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
for
|
|
117
|
-
if
|
|
113
|
+
under = str(resolved.relative_to(project.root))
|
|
114
|
+
faces = sorted(
|
|
115
|
+
project.root / pf.relpath
|
|
116
|
+
for pf in project.iter_faces(under=under, recursive=True)
|
|
117
|
+
if pf.is_yaml
|
|
118
|
+
and not pf.is_private
|
|
119
|
+
and not pf.sibling(INSPECT_TEMPLATE_MANIFEST).exists()
|
|
118
120
|
)
|
|
119
|
-
if not
|
|
121
|
+
if not faces:
|
|
120
122
|
return [
|
|
121
123
|
ValidateResult(
|
|
122
124
|
success=False,
|
|
@@ -129,7 +131,7 @@ def _validate_one_path(
|
|
|
129
131
|
],
|
|
130
132
|
)
|
|
131
133
|
]
|
|
132
|
-
return [validate(f, project=project) for f in
|
|
134
|
+
return [validate(f, project=project) for f in faces]
|
|
133
135
|
|
|
134
136
|
|
|
135
137
|
def validate(path: Path, *, project: Project) -> ValidateResult:
|
|
@@ -163,7 +165,7 @@ def validate(path: Path, *, project: Project) -> ValidateResult:
|
|
|
163
165
|
)
|
|
164
166
|
|
|
165
167
|
try:
|
|
166
|
-
result = compile_file(resolved, project=project)
|
|
168
|
+
result = compile_file(project.file_for_path(resolved), project=project)
|
|
167
169
|
except OSError as exc:
|
|
168
170
|
return ValidateResult(
|
|
169
171
|
success=False,
|
dataface/ai/agent.py
CHANGED
|
@@ -129,6 +129,8 @@ Skip this step when you are previewing ephemerally — see "Previewing vs. savin
|
|
|
129
129
|
|
|
130
130
|
Fix any errors `{{ s_validate_dashboard }}` reports, then re-run until it passes.
|
|
131
131
|
|
|
132
|
+
**YAML style:** write block style — one key per line. Avoid JSON-like inline flow maps (`{ type: bar, x: month }`); they read and diff worse than indented blocks. Inline arrays (`y: [revenue, cost]`) are fine.
|
|
133
|
+
|
|
132
134
|
## Parameterized Queries — Use From the Start
|
|
133
135
|
|
|
134
136
|
Use `{{ variable_name }}` syntax for any configurable values, even during exploration:
|
|
@@ -6,7 +6,7 @@ ready for execution and rendering.
|
|
|
6
6
|
|
|
7
7
|
Entry Points:
|
|
8
8
|
- compile(yaml_content: str) -> CompileResult
|
|
9
|
-
- compile_file(
|
|
9
|
+
- compile_file(face_file: ProjectFile, *, project: Project) -> CompileResult
|
|
10
10
|
|
|
11
11
|
Outputs:
|
|
12
12
|
- Face: Fully normalized face with all references resolved
|
dataface/core/compile/channel.py
CHANGED
|
@@ -130,7 +130,7 @@ def validate_channel_fields(
|
|
|
130
130
|
)
|
|
131
131
|
|
|
132
132
|
|
|
133
|
-
_COLOR_CHANNELS = frozenset({"color"})
|
|
133
|
+
_COLOR_CHANNELS = frozenset({"color", "background"})
|
|
134
134
|
_NUMERIC_CHANNELS: frozenset[str] = frozenset()
|
|
135
135
|
|
|
136
136
|
|
|
@@ -165,8 +165,10 @@ def normalize_chart_channels(
|
|
|
165
165
|
) -> dict[str, ResolvedStyleChannel]:
|
|
166
166
|
"""Parse and validate chart-level style channels.
|
|
167
167
|
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
``color`` is a data channel at chart root for all chart types.
|
|
169
|
+
``background`` is a data channel at chart root for KPI charts only
|
|
170
|
+
(gradient background painted by value position in scale).
|
|
171
|
+
Other paint fields (opacity, stroke) are not data channels.
|
|
170
172
|
|
|
171
173
|
``style_color``: when a ``StyleColorConfig`` with a scale is provided, the
|
|
172
174
|
series color channel is upgraded to gradient mode. A plain string value
|
|
@@ -182,6 +184,11 @@ def normalize_chart_channels(
|
|
|
182
184
|
if color_raw is not None:
|
|
183
185
|
channels["color"] = parse_style_channel(color_raw, "color")
|
|
184
186
|
|
|
187
|
+
if chart_type == "kpi":
|
|
188
|
+
background_raw = chart.background
|
|
189
|
+
if background_raw is not None:
|
|
190
|
+
channels["background"] = parse_style_channel(background_raw, "background")
|
|
191
|
+
|
|
185
192
|
# Upgrade series channel to gradient when style.color carries a scale.
|
|
186
193
|
if isinstance(style_color, StyleColorConfig):
|
|
187
194
|
if "color" not in channels:
|
|
@@ -7,7 +7,7 @@ ready for execution and rendering.
|
|
|
7
7
|
Entry Points:
|
|
8
8
|
- compile(yaml_content: str) -> CompileResult
|
|
9
9
|
- compile_authored_face(authored: AuthoredFace) -> CompileResult
|
|
10
|
-
- compile_file(
|
|
10
|
+
- compile_file(face_file: ProjectFile, *, project: Project) -> CompileResult
|
|
11
11
|
|
|
12
12
|
This is the main orchestrator for compilation. It:
|
|
13
13
|
1. Parses YAML to AuthoredFace (parser.py)
|
|
@@ -32,7 +32,6 @@ from __future__ import annotations
|
|
|
32
32
|
|
|
33
33
|
import logging
|
|
34
34
|
from dataclasses import dataclass, field
|
|
35
|
-
from pathlib import Path
|
|
36
35
|
from typing import TYPE_CHECKING, Any
|
|
37
36
|
|
|
38
37
|
import yaml
|
|
@@ -462,7 +461,7 @@ def validate_compiled_queries(
|
|
|
462
461
|
|
|
463
462
|
|
|
464
463
|
def compile_file(
|
|
465
|
-
|
|
464
|
+
face_file: ProjectFile,
|
|
466
465
|
apply_meta: bool = True,
|
|
467
466
|
*,
|
|
468
467
|
project: Project,
|
|
@@ -474,34 +473,30 @@ def compile_file(
|
|
|
474
473
|
applies meta.yaml cascading configuration from parent directories.
|
|
475
474
|
|
|
476
475
|
Args:
|
|
477
|
-
|
|
476
|
+
face_file: ProjectFile handle for the YAML (or Markdown) face file
|
|
478
477
|
apply_meta: If True, resolve and apply meta.yaml chain (default: True)
|
|
479
478
|
project: Project for meta resolution and source loading
|
|
480
|
-
markdown_metadata_table: When True and
|
|
479
|
+
markdown_metadata_table: When True and face_file is a .md, prepend
|
|
481
480
|
non-face frontmatter keys as a metadata table before the body.
|
|
482
481
|
|
|
483
482
|
Returns:
|
|
484
483
|
CompileResult with compiled face or errors
|
|
485
484
|
|
|
486
|
-
Raises:
|
|
487
|
-
FileNotFoundError: If file doesn't exist
|
|
488
|
-
|
|
489
485
|
Example:
|
|
490
|
-
>>> result = compile_file(
|
|
486
|
+
>>> result = compile_file(project.file("face.yml"), project=project)
|
|
491
487
|
>>> if result.success:
|
|
492
488
|
... print(result.face.title)
|
|
493
489
|
"""
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if not file_path.exists():
|
|
490
|
+
if not face_file.exists():
|
|
497
491
|
return CompileResult(
|
|
498
|
-
errors=[
|
|
492
|
+
errors=[
|
|
493
|
+
CompilationError(f"File not found: {face_file.relpath}").to_structured()
|
|
494
|
+
]
|
|
499
495
|
)
|
|
500
496
|
|
|
501
|
-
face_file = project.file_for_path(file_path)
|
|
502
|
-
|
|
503
497
|
# Markdown report files: translate to YAML before compiling.
|
|
504
|
-
# is_markdown_face
|
|
498
|
+
# is_markdown_face and resolve_meta_chain stay path-based (disk-coupled helpers,
|
|
499
|
+
# out of scope for this task); reconstruct the absolute path for them.
|
|
505
500
|
from dataface.core.compile.markdown import (
|
|
506
501
|
MARKDOWN_NOT_FACE_MESSAGE,
|
|
507
502
|
MARKDOWN_SUFFIXES,
|
|
@@ -509,14 +504,16 @@ def compile_file(
|
|
|
509
504
|
markdown_to_yaml,
|
|
510
505
|
)
|
|
511
506
|
|
|
512
|
-
|
|
513
|
-
|
|
507
|
+
abs_path = project.root / face_file.relpath
|
|
508
|
+
|
|
509
|
+
if abs_path.suffix.lower() in MARKDOWN_SUFFIXES:
|
|
510
|
+
if not is_markdown_face(abs_path):
|
|
514
511
|
return CompileResult(
|
|
515
512
|
errors=[CompilationError(MARKDOWN_NOT_FACE_MESSAGE).to_structured()]
|
|
516
513
|
)
|
|
517
514
|
|
|
518
515
|
try:
|
|
519
|
-
raw_text =
|
|
516
|
+
raw_text = face_file.read_text()
|
|
520
517
|
yaml_content = markdown_to_yaml(
|
|
521
518
|
raw_text, metadata_table=markdown_metadata_table
|
|
522
519
|
)
|
|
@@ -532,8 +529,6 @@ def compile_file(
|
|
|
532
529
|
errors=[CompilationError(f"Failed to read file: {e}").to_structured()]
|
|
533
530
|
)
|
|
534
531
|
|
|
535
|
-
file_str = str(file_path)
|
|
536
|
-
|
|
537
532
|
# Parse the face text to a mapping. Meta is deep-merged under it (when
|
|
538
533
|
# enabled) and the merged mapping is validated once as an AuthoredFace —
|
|
539
534
|
# no YAML round-trip. A malformed meta.yaml raises CompilationError immediately.
|
|
@@ -541,7 +536,7 @@ def compile_file(
|
|
|
541
536
|
try:
|
|
542
537
|
face_data = load_yaml_mapping(yaml_content)
|
|
543
538
|
if apply_meta:
|
|
544
|
-
meta_dict, lint = resolve_meta_chain(
|
|
539
|
+
meta_dict, lint = resolve_meta_chain(abs_path, root_path=project.root)
|
|
545
540
|
if lint.ignore or lint.ignore_queries:
|
|
546
541
|
meta_lint = lint
|
|
547
542
|
face_data = apply_meta_to_face(meta_dict, face_data)
|
|
@@ -549,12 +544,14 @@ def compile_file(
|
|
|
549
544
|
except ParseError as e:
|
|
550
545
|
return CompileResult(
|
|
551
546
|
errors=[
|
|
552
|
-
_stamp_error_file(err,
|
|
547
|
+
_stamp_error_file(err, face_file.relpath)
|
|
553
548
|
for err in _parse_error_to_structured(e, yaml_content)
|
|
554
549
|
]
|
|
555
550
|
)
|
|
556
551
|
except CompilationError as e:
|
|
557
|
-
return CompileResult(
|
|
552
|
+
return CompileResult(
|
|
553
|
+
errors=[_stamp_error_file(e.to_structured(), face_file.relpath)]
|
|
554
|
+
)
|
|
558
555
|
|
|
559
556
|
result = _compile_with_text(
|
|
560
557
|
face,
|
|
@@ -566,7 +563,7 @@ def compile_file(
|
|
|
566
563
|
# Stamp file path and add validate next_command on each compile error so
|
|
567
564
|
# UI surfaces and CLI can surface "dft validate <file>" without re-deriving the path.
|
|
568
565
|
if result.errors:
|
|
569
|
-
result.errors = [_stamp_error_file(e,
|
|
566
|
+
result.errors = [_stamp_error_file(e, face_file.relpath) for e in result.errors]
|
|
570
567
|
return result
|
|
571
568
|
|
|
572
569
|
|
|
@@ -40,6 +40,7 @@ from dataface.core.compile.models.chart.authored import (
|
|
|
40
40
|
ChartDataTablePerSeries,
|
|
41
41
|
ChartDataTableSource,
|
|
42
42
|
)
|
|
43
|
+
from dataface.core.compile.models.chart.resolved import FormatState
|
|
43
44
|
from dataface.core.compile.models.style.resolved import (
|
|
44
45
|
ResolvedChartsStyle,
|
|
45
46
|
deep_merge,
|
|
@@ -274,7 +275,7 @@ def _inter_row_rule_y_pixel(
|
|
|
274
275
|
|
|
275
276
|
def _vl_format_calc(
|
|
276
277
|
source: str,
|
|
277
|
-
format_spec:
|
|
278
|
+
format_spec: FormatState,
|
|
278
279
|
as_name: str,
|
|
279
280
|
formats: dict[str, str] | None = None,
|
|
280
281
|
) -> dict[str, Any]:
|
|
@@ -417,6 +418,7 @@ def _row_text_layer(
|
|
|
417
418
|
style: DataTableStyle,
|
|
418
419
|
charts_style: ResolvedChartsStyle,
|
|
419
420
|
spec_height: float,
|
|
421
|
+
value_format: FormatState = None,
|
|
420
422
|
sampling_step: int = 1,
|
|
421
423
|
dx: float | None = None,
|
|
422
424
|
label_period_filter_expr: str | None = None,
|
|
@@ -457,12 +459,7 @@ def _row_text_layer(
|
|
|
457
459
|
# Sampling after period filter: thin further if still over the cap.
|
|
458
460
|
if sampling_step > 1:
|
|
459
461
|
transforms.extend(_sampling_transforms(x_field, sampling_step))
|
|
460
|
-
|
|
461
|
-
transforms.append(
|
|
462
|
-
_vl_format_calc(agg_name, entry.format, cell_name, formats)
|
|
463
|
-
)
|
|
464
|
-
else:
|
|
465
|
-
transforms.append(_vl_format_calc(agg_name, None, cell_name, formats))
|
|
462
|
+
transforms.append(_vl_format_calc(agg_name, value_format, cell_name, formats))
|
|
466
463
|
else:
|
|
467
464
|
# Source entry: G1 guarantees 1 row per x.
|
|
468
465
|
# Period filter before sampling: thin to label-period openers first.
|
|
@@ -470,18 +467,21 @@ def _row_text_layer(
|
|
|
470
467
|
transforms.append({"filter": label_period_filter_expr})
|
|
471
468
|
if sampling_step > 1:
|
|
472
469
|
transforms.extend(_sampling_transforms(x_field, sampling_step))
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
)
|
|
477
|
-
else:
|
|
478
|
-
transforms.append(_vl_format_calc(entry.source, None, cell_name, formats))
|
|
470
|
+
transforms.append(
|
|
471
|
+
_vl_format_calc(entry.source, value_format, cell_name, formats)
|
|
472
|
+
)
|
|
479
473
|
|
|
480
474
|
y_pixel = _row_y_pixel(index, style, charts_style, spec_height)
|
|
481
475
|
encoding: dict[str, Any] = {
|
|
482
476
|
"x": _shared_x_encoding(parent_x_enc),
|
|
483
477
|
"y": {"value": y_pixel},
|
|
484
478
|
"text": {"field": cell_name},
|
|
479
|
+
# Opt out of inherited color encoding. Without this, VL sees own data
|
|
480
|
+
# that lacks the series field after aggregation (groupby omits the color
|
|
481
|
+
# field), adds null to the categorical domain, and null sorts first —
|
|
482
|
+
# consuming palette[0] and shifting every series mark one slot up (the
|
|
483
|
+
# dark-companion / endpoint-label off-by-one).
|
|
484
|
+
"color": None,
|
|
485
485
|
}
|
|
486
486
|
layer: dict[str, Any] = {
|
|
487
487
|
"mark": _text_mark_props(style, dx=dx),
|
|
@@ -504,6 +504,7 @@ def _per_series_row_layers(
|
|
|
504
504
|
charts_style: ResolvedChartsStyle,
|
|
505
505
|
spec_height: float,
|
|
506
506
|
spec_width: float | None,
|
|
507
|
+
value_format: FormatState = None,
|
|
507
508
|
sampling_step: int = 1,
|
|
508
509
|
dx: float | None = None,
|
|
509
510
|
label_period_filter_expr: str | None = None,
|
|
@@ -569,7 +570,7 @@ def _per_series_row_layers(
|
|
|
569
570
|
_sampling_transforms(parent_x_enc["field"], sampling_step)
|
|
570
571
|
)
|
|
571
572
|
bm_transforms.append(
|
|
572
|
-
_vl_format_calc(entry.per_series,
|
|
573
|
+
_vl_format_calc(entry.per_series, value_format, bm_cell_name, formats)
|
|
573
574
|
)
|
|
574
575
|
bm_y_pixel = _row_y_pixel(row_idx, style, charts_style, spec_height)
|
|
575
576
|
bm_cell_layer: dict[str, Any] = {
|
|
@@ -645,7 +646,7 @@ def _per_series_row_layers(
|
|
|
645
646
|
transforms.append({"filter": label_period_filter_expr})
|
|
646
647
|
if sampling_step > 1:
|
|
647
648
|
transforms.extend(_sampling_transforms(x_field, sampling_step))
|
|
648
|
-
transforms.append(_vl_format_calc(agg_name,
|
|
649
|
+
transforms.append(_vl_format_calc(agg_name, value_format, cell_name, formats))
|
|
649
650
|
|
|
650
651
|
y_pixel = _row_y_pixel(row_idx, style, charts_style, spec_height)
|
|
651
652
|
cell_layer: dict[str, Any] = {
|
|
@@ -975,6 +976,7 @@ def attach_data_table(
|
|
|
975
976
|
series_palette: list[str] | None = None,
|
|
976
977
|
label_period_filter_expr: str | None = None,
|
|
977
978
|
axis_y_orient: Literal["left", "right"],
|
|
979
|
+
entry_formats: list[FormatState] | None = None,
|
|
978
980
|
formats: dict[str, str] | None = None,
|
|
979
981
|
) -> dict[str, Any]:
|
|
980
982
|
"""Append attached-data-table layers to a Vega-Lite chart spec.
|
|
@@ -1160,6 +1162,7 @@ def attach_data_table(
|
|
|
1160
1162
|
visual_row_idx = 0
|
|
1161
1163
|
for i, entry in enumerate(data_table.entries):
|
|
1162
1164
|
row_dx = entry_dx[i] if entry_dx is not None else None
|
|
1165
|
+
row_format = entry_formats[i] if entry_formats is not None else entry.format
|
|
1163
1166
|
if isinstance(entry, ChartDataTablePerSeries):
|
|
1164
1167
|
if entry.by_measure:
|
|
1165
1168
|
# by_measure: one row per entry, no series expansion needed.
|
|
@@ -1174,11 +1177,13 @@ def attach_data_table(
|
|
|
1174
1177
|
charts_style=charts_style,
|
|
1175
1178
|
spec_height=spec_height,
|
|
1176
1179
|
spec_width=spec_width,
|
|
1180
|
+
value_format=row_format,
|
|
1177
1181
|
sampling_step=sampling_step,
|
|
1178
1182
|
dx=row_dx,
|
|
1179
1183
|
label_period_filter_expr=label_period_filter_expr,
|
|
1180
1184
|
axis_y_orient=axis_y_orient,
|
|
1181
1185
|
label_limit=_label_stub_limit,
|
|
1186
|
+
formats=formats,
|
|
1182
1187
|
)
|
|
1183
1188
|
layers.extend(bm_layers)
|
|
1184
1189
|
if style.row.rule.width > 0 and visual_row_idx < n_visual_rows - 1:
|
|
@@ -1211,6 +1216,7 @@ def attach_data_table(
|
|
|
1211
1216
|
charts_style=charts_style,
|
|
1212
1217
|
spec_height=spec_height,
|
|
1213
1218
|
spec_width=spec_width,
|
|
1219
|
+
value_format=row_format,
|
|
1214
1220
|
sampling_step=sampling_step,
|
|
1215
1221
|
dx=row_dx,
|
|
1216
1222
|
label_period_filter_expr=label_period_filter_expr,
|
|
@@ -1250,6 +1256,7 @@ def attach_data_table(
|
|
|
1250
1256
|
style=style,
|
|
1251
1257
|
charts_style=charts_style,
|
|
1252
1258
|
spec_height=spec_height,
|
|
1259
|
+
value_format=row_format,
|
|
1253
1260
|
sampling_step=sampling_step,
|
|
1254
1261
|
dx=row_dx,
|
|
1255
1262
|
label_period_filter_expr=label_period_filter_expr,
|