dataface 0.3.2.dev22__py3-none-any.whl → 0.3.2.dev150__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dataface/DATAFACE_SYNTAX.md +15 -4
- dataface/__init__.py +70 -23
- dataface/agent_api/__init__.py +162 -68
- dataface/agent_api/_paths.py +9 -3
- dataface/agent_api/docs/yaml-reference.md +273 -239
- dataface/agent_api/init.py +1 -1
- dataface/agent_api/project_session.py +2 -1
- dataface/agent_api/serve.py +113 -0
- dataface/agent_api/validate.py +8 -0
- dataface/ai/agent.py +28 -2
- dataface/cli/_project.py +100 -1
- dataface/cli/commands/board_artifact.py +5 -2
- dataface/cli/commands/describe.py +7 -3
- dataface/cli/commands/docs.py +12 -3
- dataface/cli/commands/init.py +7 -14
- dataface/cli/commands/inspect.py +6 -1
- dataface/cli/commands/mcp_init.py +23 -43
- dataface/cli/commands/migrate.py +8 -3
- dataface/cli/commands/query.py +11 -8
- dataface/cli/commands/render.py +26 -9
- dataface/cli/commands/search.py +8 -2
- dataface/cli/commands/serve.py +25 -81
- dataface/cli/commands/skills_init.py +2 -19
- dataface/cli/commands/validate.py +7 -2
- dataface/cli/filesystem_project.py +5 -4
- dataface/cli/main.py +22 -33
- dataface/core/__init__.py +73 -28
- dataface/core/aliases.py +27 -0
- dataface/core/{compile/colors.py → colors.py} +20 -5
- dataface/core/compile/__init__.py +0 -8
- dataface/core/compile/channel.py +0 -13
- dataface/core/compile/compiler.py +161 -96
- dataface/core/compile/data_table.py +7 -2
- dataface/core/compile/enrich.py +18 -44
- dataface/core/compile/face_warnings.py +46 -8
- dataface/core/compile/format.py +47 -1
- dataface/core/compile/inherit_graph.py +3 -3
- dataface/core/compile/inherit_registry.yaml +1 -1
- dataface/core/compile/introspection.py +2 -13
- dataface/core/compile/migrations.py +112 -9
- dataface/core/compile/models/chart/authored/_base.py +1 -1
- dataface/core/compile/models/chart/normalized/__init__.py +2 -0
- dataface/core/compile/models/chart/normalized/_base.py +26 -0
- dataface/core/compile/models/chart/normalized/kpi.py +0 -4
- dataface/core/compile/models/chart/resolved/_base.py +28 -10
- dataface/core/compile/models/chart/resolved/callout.py +2 -3
- dataface/core/compile/models/chart/resolved/kpi.py +7 -3
- dataface/core/compile/models/chart/resolved/table.py +16 -2
- dataface/core/compile/models/config.py +5 -0
- dataface/core/compile/models/face/authored.py +24 -89
- dataface/core/compile/models/markers.py +1 -1
- dataface/core/compile/models/primitives.py +175 -2
- dataface/core/compile/models/query/authored.py +1 -119
- dataface/core/compile/models/refs.py +0 -2
- dataface/core/compile/models/source.py +78 -164
- dataface/core/compile/models/style/authored/__init__.py +32 -8
- dataface/core/compile/models/style/authored/_base.py +126 -69
- dataface/core/compile/models/style/authored/table.py +61 -3
- dataface/core/compile/models/style/resolved/__init__.py +12 -2
- dataface/core/compile/models/style/resolved/_base.py +142 -57
- dataface/core/compile/models/style/resolved/_cartesian.py +6 -11
- dataface/core/compile/models/style/resolved/_cascade.py +141 -54
- dataface/core/compile/models/style/resolved/area.py +1 -1
- dataface/core/compile/models/style/resolved/bar.py +1 -1
- dataface/core/compile/models/style/resolved/line.py +1 -1
- dataface/core/compile/models/style/resolved/scatter.py +3 -2
- dataface/core/compile/models/style/theme/__init__.py +36 -14
- dataface/core/compile/models/style/theme/_chart_base.py +17 -9
- dataface/core/compile/models/style/theme/axis.py +432 -312
- dataface/core/compile/models/style/theme/charts.py +10 -6
- dataface/core/compile/models/style/theme/kpi.py +2 -1
- dataface/core/compile/models/style/theme/page.py +5 -2
- dataface/core/compile/models/style/theme/table.py +2 -2
- dataface/core/compile/models/style/theme/variables.py +0 -3
- dataface/core/compile/models/vega_lite/config.py +1 -1
- dataface/core/compile/normalize_charts.py +10 -154
- dataface/core/compile/normalize_layout.py +20 -284
- dataface/core/compile/normalize_queries.py +24 -513
- dataface/core/compile/normalizer.py +4 -10
- dataface/core/compile/palette.py +19 -7
- dataface/core/compile/resolve/__init__.py +2 -0
- dataface/core/compile/resolve/_axes.py +147 -44
- dataface/core/compile/resolve/_channels.py +1 -1
- dataface/core/compile/resolve/_dispatch.py +1 -1
- dataface/core/compile/resolve/_domain.py +49 -32
- dataface/core/compile/resolve/_kwargs.py +39 -12
- dataface/core/compile/resolve/_marks.py +8 -3
- dataface/core/compile/resolve/_palette.py +7 -3
- dataface/core/compile/resolve/_table.py +124 -33
- dataface/core/compile/resolve/area.py +48 -18
- dataface/core/compile/resolve/bar.py +245 -64
- dataface/core/compile/resolve/geo.py +10 -3
- dataface/core/compile/resolve/heatmap.py +16 -8
- dataface/core/compile/resolve/line.py +35 -12
- dataface/core/compile/resolve/pie.py +4 -1
- dataface/core/compile/resolve/pie_attachment.py +1 -1
- dataface/core/compile/resolve/scatter.py +25 -13
- dataface/core/compile/resolve/simple.py +25 -2
- dataface/core/compile/schema_renderers/prompt.py +1 -1
- dataface/core/compile/schema_renderers/textmate_grammar.py +8 -1
- dataface/core/compile/source_map.py +211 -28
- dataface/core/compile/style_cascade.py +126 -55
- dataface/core/compile/validator.py +121 -44
- dataface/core/compile/yaml_error_formatter.py +108 -3
- dataface/core/connections.py +10 -3
- dataface/core/dashboard.py +27 -19
- dataface/core/defaults/default_config.yml +4 -0
- dataface/core/defaults/themes/_base.yaml +49 -40
- dataface/core/defaults/themes/cream.yaml +4 -4
- dataface/core/defaults/themes/diagnostics-title-baseline.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +1 -1
- dataface/core/defaults/themes/editorial.yaml +9 -6
- dataface/core/defaults/themes/neon.yaml +15 -8
- dataface/core/defaults/themes/plain.yaml +6 -4
- dataface/core/defaults/themes/stark.yaml +24 -7
- dataface/core/defaults/themes/vivid.yaml +15 -8
- dataface/core/diagnostics/__init__.py +12 -2
- dataface/core/diagnostics/codes_compile.py +17 -33
- dataface/core/diagnostics/codes_execute.py +149 -0
- dataface/core/diagnostics/codes_render.py +144 -31
- dataface/core/diagnostics/diagnostic.py +30 -0
- dataface/core/diagnostics/hints.py +19 -0
- dataface/core/dialects/base.py +17 -0
- dataface/core/dialects/postgres.py +14 -0
- dataface/core/dialects/redshift.py +5 -0
- dataface/core/execute/__init__.py +59 -14
- dataface/core/execute/adapters/__init__.py +1 -1
- dataface/core/execute/adapters/dbt_adapter.py +4 -1
- dataface/core/execute/adapters/dbt_adapter_factory.py +19 -0
- dataface/core/execute/adapters/dbt_utils.py +84 -20
- dataface/core/execute/adapters/postgres_connection_manager.py +48 -0
- dataface/core/execute/adapters/sql_adapter.py +72 -4
- dataface/core/execute/cache_keys.py +4 -9
- dataface/core/execute/dbt_jinja.py +65 -10
- dataface/core/execute/executor.py +94 -19
- dataface/core/execute/file_source_materializer.py +185 -32
- dataface/core/execute/trivial_local_cache.py +5 -2
- dataface/core/font_measure.py +28 -0
- dataface/core/fonts.py +8 -5
- dataface/core/inspect/__init__.py +46 -7
- dataface/core/project.py +19 -3
- dataface/core/render/chart/data_table_attachment.py +14 -12
- dataface/core/render/chart/emitters/_cartesian.py +21 -18
- dataface/core/render/chart/emitters/_channels.py +56 -8
- dataface/core/render/chart/emitters/_label_overlap.py +18 -15
- dataface/core/render/chart/emitters/_measured_label_padding.py +4 -4
- dataface/core/render/chart/emitters/area.py +3 -3
- dataface/core/render/chart/emitters/bar.py +60 -35
- dataface/core/render/chart/emitters/geo.py +8 -13
- dataface/core/render/chart/emitters/heatmap.py +20 -26
- dataface/core/render/chart/emitters/line.py +2 -2
- dataface/core/render/chart/emitters/scatter.py +7 -30
- dataface/core/render/chart/features/baseline.py +22 -9
- dataface/core/render/chart/features/endpoint_labels.py +55 -32
- dataface/core/render/chart/features/mirror_axis.py +12 -9
- dataface/core/render/chart/features/value_labels.py +1 -1
- dataface/core/render/chart/kpi.py +11 -90
- dataface/core/render/chart/mark_extents.py +339 -0
- dataface/core/render/chart/presentation.py +6 -8
- dataface/core/render/chart/rendering.py +100 -46
- dataface/core/render/chart/session.py +10 -0
- dataface/core/render/chart/spec.py +3 -0
- dataface/core/render/chart/spec_builders.py +18 -18
- dataface/core/render/chart/table.py +81 -36
- dataface/core/render/chart/table_support.py +2 -53
- dataface/core/render/chart/time_unit_detect.py +3 -2
- dataface/core/render/chart/translate.py +82 -44
- dataface/core/render/chart/type_inference.py +45 -25
- dataface/core/render/chart/validation.py +91 -0
- dataface/core/render/chart/vega_lite.py +30 -11
- dataface/core/render/chart/vl_field_maps.py +111 -76
- dataface/core/render/conditions.py +122 -0
- dataface/core/render/controls.py +60 -187
- dataface/core/render/converters/chart.py +16 -6
- dataface/core/render/data_format.py +1 -1
- dataface/core/render/face_resolve.py +7 -0
- dataface/core/render/faces.py +97 -32
- dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.woff2 +0 -0
- dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.woff2 +0 -0
- dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -93
- dataface/core/render/fonts/_font_face.css +10 -0
- dataface/core/render/layout_sizing.py +313 -238
- dataface/core/render/numeral_expr.py +80 -0
- dataface/core/render/renderer.py +14 -1
- dataface/core/render/sizing.py +42 -105
- dataface/core/render/svg_utils.py +60 -14
- dataface/core/render/terminal.py +1 -1
- dataface/core/render/variables_layout.py +211 -0
- dataface/core/render/variables_resolve.py +376 -0
- dataface/core/render/variables_strip.py +291 -368
- dataface/core/render/warnings/bar_band_width_too_narrow.py +64 -6
- dataface/core/render/warnings/base.py +18 -0
- dataface/core/render/warnings/layered_chart_shared_y_axis_scale_mismatch.py +25 -3
- dataface/core/render/warnings/layout_min_exceeds_height.py +68 -0
- dataface/core/render/warnings/likely_currency_or_percent_missing_formatter.py +12 -6
- dataface/core/render/warnings/palette_unsupported.py +6 -0
- dataface/core/render/warnings/pie_dominant_segment.py +1 -0
- dataface/core/render/warnings/pie_too_many_segments.py +1 -0
- dataface/core/render/warnings/point_map_out_of_projection.py +1 -0
- dataface/core/render/warnings/query_returned_zero_rows.py +1 -0
- dataface/core/render/warnings/redundant_encoding.py +20 -1
- dataface/core/render/warnings/registry.py +9 -3
- dataface/core/render/warnings/temporal_single_point.py +1 -0
- dataface/core/render/warnings/too_many_color_categories.py +1 -0
- dataface/core/render/warnings/too_many_x_categories.py +18 -2
- dataface/core/render/warnings/value_labels_crowd_width.py +9 -0
- dataface/core/render/warnings/y_encoding_mostly_null.py +1 -0
- dataface/core/render_format.py +18 -0
- dataface/core/serve/alias_index.py +16 -22
- dataface/core/text/format_d3.py +56 -0
- dataface/core/text/numeral_scale.py +218 -0
- dataface/core/utils.py +103 -10
- dataface/data/schemas/board-resolved/board-resolved.schema.json +8589 -7578
- dataface/data/schemas/yaml/0.3.2.json +1 -0
- dataface/data/schemas/yaml/manifest.json +7 -0
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/METADATA +1 -3
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/RECORD +223 -213
- dataface/core/inspect/partials/categorical.yml +0 -38
- dataface/core/inspect/partials/date.yml +0 -38
- dataface/core/inspect/partials/numeric.yml +0 -53
- dataface/core/inspect/templates/charts.yml +0 -94
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/WHEEL +0 -0
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/entry_points.txt +0 -0
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import Literal
|
|
5
|
+
from typing import Any, Literal
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel
|
|
8
8
|
|
|
9
|
+
from dataface.core.compile.models.style.resolved._base import ResolvedScaleStyle
|
|
9
10
|
from dataface.core.compile.models.style.theme.marks import BarMarkStyle
|
|
10
11
|
from dataface.core.compile.tick_values import numeric_domain_bounds
|
|
11
12
|
from dataface.core.render.chart._types import VLDict
|
|
@@ -19,40 +20,76 @@ from dataface.core.render.chart.emitters._measured_label_padding import (
|
|
|
19
20
|
quantitative_tick_labels,
|
|
20
21
|
)
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
# (``config.scale.bandPaddingInner``) — they are silently ignored when
|
|
27
|
-
# placed on ``encoding.{x,y}.scale``.
|
|
28
|
-
"band_padding_inner": "paddingInner",
|
|
29
|
-
"band_padding_outer": "paddingOuter",
|
|
30
|
-
"point_padding": "pointPadding",
|
|
31
|
-
"rect_band_padding_inner": "rectBandPaddingInner",
|
|
32
|
-
"round": "round",
|
|
23
|
+
# ``type``/log.base/pow.exponent/symlog.constant emit log/pow/symlog scale
|
|
24
|
+
# config. Must not be omitted — a missed field silently resolves to None and
|
|
25
|
+
# drops scale config from the emitted spec.
|
|
26
|
+
_SCALE_CONTINUOUS_FIELD_MAP: dict[str, str] = {
|
|
33
27
|
"zero": "zero",
|
|
34
|
-
"clamp": "clamp",
|
|
35
|
-
"continuous_padding": "continuousPadding",
|
|
36
|
-
"quantile_count": "quantileCount",
|
|
37
|
-
"quantize_count": "quantizeCount",
|
|
38
|
-
"use_unaggregated_domain": "useUnaggregatedDomain",
|
|
39
|
-
"x_reverse": "xReverse",
|
|
40
28
|
"type": "type",
|
|
41
|
-
|
|
42
|
-
"exponent": "exponent",
|
|
43
|
-
"constant": "constant",
|
|
29
|
+
# domain and nice are encoding-level only (see SCALE_ENCODING_FIELD_MAP).
|
|
44
30
|
}
|
|
45
|
-
|
|
46
|
-
|
|
31
|
+
# Universal top-level scale fields (not in any sub-group).
|
|
32
|
+
_SCALE_UNIVERSAL_FIELD_MAP: dict[str, str] = {
|
|
33
|
+
"round": "round",
|
|
34
|
+
"clamp": "clamp",
|
|
35
|
+
}
|
|
36
|
+
# Encoding-level-only scale fields: nice, padding shorthand. domain is read
|
|
37
|
+
# explicitly from scale.continuous.domain below (nested, not a flat field).
|
|
38
|
+
# Applied to encoding.x.scale / encoding.y.scale only (not VL config).
|
|
47
39
|
SCALE_ENCODING_FIELD_MAP: dict[str, str] = {
|
|
48
|
-
"domain": "domain",
|
|
49
40
|
"nice": "nice",
|
|
50
41
|
# Unified shorthand. VL dispatches based on scale type (bandPaddingOuter,
|
|
51
42
|
# pointPadding, continuousPadding). Encoding-level only.
|
|
52
43
|
"padding": "padding",
|
|
53
44
|
}
|
|
45
|
+
# x-channel-only scale fields.
|
|
46
|
+
_SCALE_X_ONLY_FIELD_MAP: dict[str, str] = {
|
|
47
|
+
"x_reverse": "xReverse",
|
|
48
|
+
}
|
|
49
|
+
|
|
54
50
|
|
|
55
|
-
|
|
51
|
+
def emit_resolved_scale_vl(
|
|
52
|
+
scale: ResolvedScaleStyle | None,
|
|
53
|
+
*,
|
|
54
|
+
include_encoding_fields: bool = True,
|
|
55
|
+
include_x_only: bool = False,
|
|
56
|
+
) -> dict[str, Any]:
|
|
57
|
+
"""Emit a VL scale dict from a ResolvedScaleStyle (non-None fields only).
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
scale: Resolved scale to emit. Returns {} when None.
|
|
61
|
+
include_encoding_fields: When True (default), includes domain/nice/padding
|
|
62
|
+
(encoding-level-only fields). Set False for VL config-level emit.
|
|
63
|
+
include_x_only: When True, includes xReverse (x-channel only).
|
|
64
|
+
"""
|
|
65
|
+
if scale is None:
|
|
66
|
+
return {}
|
|
67
|
+
out: dict[str, Any] = {}
|
|
68
|
+
out.update(map_fields(scale, _SCALE_UNIVERSAL_FIELD_MAP))
|
|
69
|
+
if scale.continuous is not None:
|
|
70
|
+
out.update(map_fields(scale.continuous, _SCALE_CONTINUOUS_FIELD_MAP))
|
|
71
|
+
if scale.continuous.log is not None and scale.continuous.log.base is not None:
|
|
72
|
+
out["base"] = scale.continuous.log.base
|
|
73
|
+
if (
|
|
74
|
+
scale.continuous.pow is not None
|
|
75
|
+
and scale.continuous.pow.exponent is not None
|
|
76
|
+
):
|
|
77
|
+
out["exponent"] = scale.continuous.pow.exponent
|
|
78
|
+
if (
|
|
79
|
+
scale.continuous.symlog is not None
|
|
80
|
+
and scale.continuous.symlog.constant is not None
|
|
81
|
+
):
|
|
82
|
+
out["constant"] = scale.continuous.symlog.constant
|
|
83
|
+
if include_encoding_fields:
|
|
84
|
+
out.update(map_fields(scale, SCALE_ENCODING_FIELD_MAP))
|
|
85
|
+
if scale.continuous is not None:
|
|
86
|
+
domain = scale.continuous.domain
|
|
87
|
+
if domain is not None:
|
|
88
|
+
out["domain"] = domain
|
|
89
|
+
if include_x_only:
|
|
90
|
+
if scale.x_reverse is not None:
|
|
91
|
+
out["xReverse"] = scale.x_reverse
|
|
92
|
+
return out
|
|
56
93
|
|
|
57
94
|
|
|
58
95
|
def _n(obj: Any, *path: str) -> Any:
|
|
@@ -97,9 +134,9 @@ def axis_to_vl(
|
|
|
97
134
|
) -> dict[str, Any]:
|
|
98
135
|
"""Map any axis style object → VL axis config dict (non-None fields only).
|
|
99
136
|
|
|
100
|
-
Works for
|
|
101
|
-
|
|
102
|
-
|
|
137
|
+
Works for any concrete axis style (all fields emitted) and any axis-variant
|
|
138
|
+
patch (skip None). Single canonical mapper for all axis variants — theme
|
|
139
|
+
global/channel/type-conditional and chart-local patches.
|
|
103
140
|
|
|
104
141
|
``label_overlap`` is the render-local VL directive from resolve_axis_x_overlap;
|
|
105
142
|
it is not stored on the resolved model (VL vocabulary stays out of Resolved*).
|
|
@@ -108,27 +145,25 @@ def axis_to_vl(
|
|
|
108
145
|
return {}
|
|
109
146
|
d: dict[str, Any] = {}
|
|
110
147
|
|
|
111
|
-
# Label font (nested via
|
|
112
|
-
if (v := _n(axis, "
|
|
148
|
+
# Label font (nested via labels.font)
|
|
149
|
+
if (v := _n(axis, "labels", "font", "color")) is not None:
|
|
113
150
|
d["labelColor"] = v
|
|
114
|
-
if (v := _n(axis, "
|
|
151
|
+
if (v := _n(axis, "labels", "font", "family")) is not None:
|
|
115
152
|
d["labelFont"] = v
|
|
116
|
-
if (v := _n(axis, "
|
|
153
|
+
if (v := _n(axis, "labels", "font", "size")) is not None:
|
|
117
154
|
d["labelFontSize"] = v
|
|
118
|
-
if (v := _n(axis, "
|
|
155
|
+
if (v := _n(axis, "labels", "font", "weight")) is not None:
|
|
119
156
|
d["labelFontWeight"] = v
|
|
120
|
-
if (v := _n(axis, "
|
|
157
|
+
if (v := _n(axis, "labels", "padding")) is not None:
|
|
121
158
|
d["labelPadding"] = v
|
|
122
|
-
if (v := _n(axis, "
|
|
159
|
+
if (v := _n(axis, "labels", "max_width")) is not None:
|
|
123
160
|
d["labelLimit"] = v
|
|
124
161
|
if label_angle is not None:
|
|
125
162
|
d["labelAngle"] = label_angle
|
|
126
|
-
elif (v := _n(axis, "
|
|
163
|
+
elif (v := _n(axis, "labels", "angle")) is not None:
|
|
127
164
|
d["labelAngle"] = v
|
|
128
|
-
if (v := _n(axis, "
|
|
165
|
+
if (v := _n(axis, "labels", "align")) is not None:
|
|
129
166
|
d["labelAlign"] = v
|
|
130
|
-
if (v := _n(axis, "label", "baseline")) is not None:
|
|
131
|
-
d["labelBaseline"] = v
|
|
132
167
|
# label_overlap is the render-local VL directive from resolve_axis_x_overlap.
|
|
133
168
|
# "allow" → false (no reduction; labels may overlap).
|
|
134
169
|
# "parity" → pass through as-is (VL drops every other label).
|
|
@@ -137,22 +172,20 @@ def axis_to_vl(
|
|
|
137
172
|
d["labelOverlap"] = False
|
|
138
173
|
elif label_overlap == "parity":
|
|
139
174
|
d["labelOverlap"] = label_overlap
|
|
140
|
-
if (v := _n(axis, "
|
|
175
|
+
if (v := _n(axis, "labels", "min_gap")) is not None:
|
|
141
176
|
d["labelSeparation"] = v
|
|
142
|
-
if (v := _n(axis, "
|
|
177
|
+
if (v := _n(axis, "labels", "visible")) is not None:
|
|
143
178
|
d["labels"] = v
|
|
144
|
-
if (v := _n(axis, "
|
|
179
|
+
if (v := _n(axis, "labels", "expr")) is not None:
|
|
145
180
|
d["labelExpr"] = v
|
|
146
|
-
if (v := _n(axis, "
|
|
181
|
+
if (v := _n(axis, "labels", "bound")) is not None:
|
|
147
182
|
d["labelBound"] = v
|
|
148
|
-
if (v := _n(axis, "
|
|
183
|
+
if (v := _n(axis, "labels", "flush")) is not None:
|
|
149
184
|
d["labelFlush"] = v
|
|
150
|
-
if (v := _n(axis, "
|
|
185
|
+
if (v := _n(axis, "labels", "offset")) is not None:
|
|
151
186
|
d["labelOffset"] = v
|
|
152
|
-
if (v := _n(axis, "
|
|
153
|
-
d["
|
|
154
|
-
if (v := _n(axis, "label", "anchor")) is not None:
|
|
155
|
-
d["labelAnchor"] = v
|
|
187
|
+
if (v := _n(axis, "labels", "format")) is not None:
|
|
188
|
+
d["format"] = v
|
|
156
189
|
|
|
157
190
|
# Title: null suppresses the visual axis label (title still appears in tooltips
|
|
158
191
|
# via encoding.title). Emitting null is the correct VL mechanism — it removes
|
|
@@ -184,12 +217,12 @@ def axis_to_vl(
|
|
|
184
217
|
if (v := _n(axis, "grid", "dash")) is not None:
|
|
185
218
|
d["gridDash"] = v
|
|
186
219
|
|
|
187
|
-
# Domain
|
|
188
|
-
if (v := _n(axis, "
|
|
220
|
+
# Domain (Dataface's `line` field — VL's axis.domain* properties)
|
|
221
|
+
if (v := _n(axis, "line", "visible")) is not None:
|
|
189
222
|
d["domain"] = v
|
|
190
|
-
if (v := _n(axis, "
|
|
223
|
+
if (v := _n(axis, "line", "width")) is not None:
|
|
191
224
|
d["domainWidth"] = v
|
|
192
|
-
if (v := _n(axis, "
|
|
225
|
+
if (v := _n(axis, "line", "color")) is not None:
|
|
193
226
|
d["domainColor"] = v
|
|
194
227
|
|
|
195
228
|
# Ticks
|
|
@@ -197,21 +230,19 @@ def axis_to_vl(
|
|
|
197
230
|
d["ticks"] = v
|
|
198
231
|
if (v := _n(axis, "ticks", "color")) is not None:
|
|
199
232
|
d["tickColor"] = v
|
|
200
|
-
if (v := _n(axis, "ticks", "
|
|
233
|
+
if (v := _n(axis, "ticks", "length")) is not None:
|
|
201
234
|
d["tickSize"] = v
|
|
202
235
|
if (v := _n(axis, "ticks", "width")) is not None:
|
|
203
236
|
d["tickWidth"] = v
|
|
237
|
+
if (v := _n(axis, "ticks", "offset")) is not None:
|
|
238
|
+
d["offset"] = v
|
|
204
239
|
|
|
205
240
|
# Flat fields
|
|
206
241
|
if (v := _n(axis, "position")) is not None:
|
|
207
242
|
d["orient"] = v
|
|
208
|
-
if (v := _n(axis, "offset")) is not None:
|
|
209
|
-
d["offset"] = v
|
|
210
243
|
if (v := _n(axis, "band_position")) is not None:
|
|
211
244
|
d["bandPosition"] = v
|
|
212
|
-
if (v := _n(axis, "
|
|
213
|
-
d["format"] = v
|
|
214
|
-
if (v := _n(axis, "values")) is not None:
|
|
245
|
+
if (v := _n(axis, "scale", "values")) is not None:
|
|
215
246
|
d["values"] = v
|
|
216
247
|
|
|
217
248
|
return d
|
|
@@ -262,28 +293,32 @@ def measure_axis_to_vl(
|
|
|
262
293
|
orient in ("left", "right")
|
|
263
294
|
and align == orient
|
|
264
295
|
and "labelExpr" not in d
|
|
265
|
-
and axis.
|
|
296
|
+
and axis.labels.font.case not in ("upper", "lower")
|
|
266
297
|
):
|
|
267
298
|
if category_labels:
|
|
268
299
|
labels = list(category_labels)
|
|
269
|
-
elif axis.format is not None:
|
|
300
|
+
elif axis.labels.format is not None:
|
|
270
301
|
if axis.tick_values:
|
|
271
|
-
labels = quantitative_tick_labels(
|
|
302
|
+
labels = quantitative_tick_labels(
|
|
303
|
+
tuple(axis.tick_values), axis.labels.format
|
|
304
|
+
)
|
|
272
305
|
else:
|
|
273
|
-
|
|
306
|
+
_sc = axis.scale
|
|
307
|
+
_sc_cont = _sc.continuous if _sc is not None else None
|
|
308
|
+
domain = _sc_cont.domain if _sc_cont is not None else None
|
|
274
309
|
bounds = numeric_domain_bounds(domain)
|
|
275
310
|
values = numeric_values(data, y_fields) + (
|
|
276
311
|
list(bounds) if bounds is not None else []
|
|
277
312
|
)
|
|
278
|
-
labels = estimated_quantitative_tick_labels(values, axis.format)
|
|
313
|
+
labels = estimated_quantitative_tick_labels(values, axis.labels.format)
|
|
279
314
|
if labels:
|
|
280
315
|
tick_size = (
|
|
281
|
-
axis.ticks.
|
|
316
|
+
axis.ticks.length if axis.ticks.length is not None else DEFAULT_VL_TICK_SIZE
|
|
282
317
|
)
|
|
283
|
-
padding = measured_label_padding(labels, axis.
|
|
318
|
+
padding = measured_label_padding(labels, axis.labels.font, tick_size)
|
|
284
319
|
label_limit = (
|
|
285
|
-
axis.
|
|
286
|
-
if axis.
|
|
320
|
+
axis.labels.max_width
|
|
321
|
+
if axis.labels.max_width is not None
|
|
287
322
|
else DEFAULT_VL_LABEL_LIMIT
|
|
288
323
|
)
|
|
289
324
|
d["labelPadding"] = cap_padding_to_label_limit(padding, label_limit, tick_size)
|
|
@@ -329,7 +364,7 @@ def inject_axis_label_case(ax_vl: dict[str, Any], axis: Any) -> dict[str, Any]:
|
|
|
329
364
|
MUST be called AFTER build_cartesian_x_encoding for x-axes so the temporal
|
|
330
365
|
smart-cadence labelExpr is already set before wrapping.
|
|
331
366
|
"""
|
|
332
|
-
case = _n(axis, "
|
|
367
|
+
case = _n(axis, "labels", "font", "case")
|
|
333
368
|
if case not in ("upper", "lower"):
|
|
334
369
|
return ax_vl
|
|
335
370
|
inner = ax_vl.get("labelExpr", "datum.label")
|
|
@@ -337,21 +372,21 @@ def inject_axis_label_case(ax_vl: dict[str, Any], axis: Any) -> dict[str, Any]:
|
|
|
337
372
|
|
|
338
373
|
|
|
339
374
|
def inject_axis_label_values_filter(ax_vl: dict[str, Any], axis: Any) -> dict[str, Any]:
|
|
340
|
-
"""Wrap labelExpr with a membership filter for authored ``
|
|
375
|
+
"""Wrap labelExpr with a membership filter for authored ``labels.values``.
|
|
341
376
|
|
|
342
|
-
Blanks any tick whose date isn't in ``axis.
|
|
377
|
+
Blanks any tick whose date isn't in ``axis.labels.values``, decoupling
|
|
343
378
|
label density from tick/grid density: tick/grid values are left
|
|
344
|
-
dense (auto-filled or explicitly authored via ``axis.values``); only
|
|
345
|
-
listed dates keep their label text. The inner expression defaults to
|
|
379
|
+
dense (auto-filled or explicitly authored via ``axis.scale.values``); only
|
|
380
|
+
the listed dates keep their label text. The inner expression defaults to
|
|
346
381
|
``datum.label`` when no labelExpr is present yet, so authoring only
|
|
347
|
-
``
|
|
382
|
+
``labels.values`` on an otherwise-plain temporal axis still respects
|
|
348
383
|
whatever ``format`` VL already applied to ``datum.label``.
|
|
349
384
|
|
|
350
385
|
MUST be called AFTER build_cartesian_x_encoding / inject_axis_label_case
|
|
351
386
|
so it wraps whatever label text (smart cadence, custom format, case) those
|
|
352
|
-
steps already produced —
|
|
387
|
+
steps already produced — labels.values always wins on top.
|
|
353
388
|
"""
|
|
354
|
-
label_values = _n(axis, "
|
|
389
|
+
label_values = _n(axis, "labels", "values")
|
|
355
390
|
if label_values is None:
|
|
356
391
|
return ax_vl
|
|
357
392
|
from dataface.core.render.chart.time_unit_detect import label_values_filter_expr
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Boolean conditions authored in YAML — ``enabled``, ``visible``.
|
|
2
|
+
|
|
3
|
+
Stage: RENDER
|
|
4
|
+
Purpose: Turn a bool, a Jinja expression, or a single-row query probe into the
|
|
5
|
+
yes/no a renderer needs.
|
|
6
|
+
|
|
7
|
+
Three surfaces share this: a variable's ``enabled``, a layout item's
|
|
8
|
+
``visible``, and the terminal renderer's copy of the same question. They differ
|
|
9
|
+
only in what a missing condition means, which is why ``if_none`` is a parameter
|
|
10
|
+
rather than a default baked in here.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
from dataface.core.compile.jinja import resolve_jinja_template
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from dataface.core.compile.models.variable.authored import SingleRowBoolProbe
|
|
21
|
+
from dataface.core.execute.chart_data_provider import ChartDataProvider
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def coerce_bool(value: Any) -> bool:
|
|
25
|
+
"""Coerce value to bool; raise ValueError for unrecognized inputs."""
|
|
26
|
+
if isinstance(value, bool):
|
|
27
|
+
return value
|
|
28
|
+
if isinstance(value, (int, float)):
|
|
29
|
+
if value == 1:
|
|
30
|
+
return True
|
|
31
|
+
if value == 0:
|
|
32
|
+
return False
|
|
33
|
+
if isinstance(value, str):
|
|
34
|
+
lower = value.strip().lower()
|
|
35
|
+
if lower in ("true", "1", "yes"):
|
|
36
|
+
return True
|
|
37
|
+
if lower in ("false", "0", "no"):
|
|
38
|
+
return False
|
|
39
|
+
raise ValueError(f"Cannot coerce {value!r} to bool; expected true/false/1/0/yes/no")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def eval_bool_condition(
|
|
43
|
+
expr: bool | str | SingleRowBoolProbe | None,
|
|
44
|
+
if_none: bool,
|
|
45
|
+
current_values: dict[str, Any],
|
|
46
|
+
executor: ChartDataProvider | None,
|
|
47
|
+
context: str,
|
|
48
|
+
) -> bool:
|
|
49
|
+
"""Evaluate a bool/str/probe condition and return the result.
|
|
50
|
+
|
|
51
|
+
Shared core for Variable.enabled and LayoutItem.visible; callers pass
|
|
52
|
+
``if_none`` to control the meaning of a missing (None) value.
|
|
53
|
+
|
|
54
|
+
- None: return ``if_none`` (enabled → True, visible → True).
|
|
55
|
+
- bool: return as-is.
|
|
56
|
+
- str: auto-wrap in ``{{ }}`` if needed, evaluate as Jinja, coerce to bool.
|
|
57
|
+
- SingleRowBoolProbe: execute the named query; expect exactly 1 row; coerce
|
|
58
|
+
the named column to bool.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
expr: The condition value (from YAML or a compiled model field).
|
|
62
|
+
if_none: Value to return when expr is None.
|
|
63
|
+
current_values: Current variable values for Jinja resolution.
|
|
64
|
+
executor: Required when expr is a SingleRowBoolProbe.
|
|
65
|
+
context: Human-readable label for error messages (e.g. "Variable 'region' disabled").
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
ValueError: Wrong row count, missing column, non-bool-coercible value, or
|
|
69
|
+
no executor for a query-backed condition.
|
|
70
|
+
"""
|
|
71
|
+
if expr is None:
|
|
72
|
+
return if_none
|
|
73
|
+
if isinstance(expr, bool):
|
|
74
|
+
return expr
|
|
75
|
+
if isinstance(expr, str):
|
|
76
|
+
# Auto-wrap bare variable names / Jinja expressions (no {{ }} required).
|
|
77
|
+
jinja_expr = expr if "{{" in expr else f"{{{{ {expr} }}}}"
|
|
78
|
+
rendered = resolve_jinja_template(jinja_expr, variables=current_values)
|
|
79
|
+
return coerce_bool(rendered)
|
|
80
|
+
if executor is None:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"{context} query '{expr.query}' requires an executor but none was provided"
|
|
83
|
+
)
|
|
84
|
+
rows = executor.execute_query(expr.query, current_values)
|
|
85
|
+
if not rows:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"{context} query '{expr.query}' returned no rows; expected exactly 1"
|
|
88
|
+
)
|
|
89
|
+
if len(rows) > 1:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"{context} query '{expr.query}' returned {len(rows)} rows; expected exactly 1"
|
|
92
|
+
)
|
|
93
|
+
row = rows[0]
|
|
94
|
+
if expr.column not in row:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"{context} query '{expr.query}' has no column '{expr.column}'; "
|
|
97
|
+
f"available: {list(row.keys())}"
|
|
98
|
+
)
|
|
99
|
+
return coerce_bool(row[expr.column])
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def evaluate_visible(
|
|
103
|
+
expr: bool | str | SingleRowBoolProbe | None,
|
|
104
|
+
current_values: dict[str, Any],
|
|
105
|
+
executor: ChartDataProvider | None,
|
|
106
|
+
context: str = "layout item",
|
|
107
|
+
) -> bool:
|
|
108
|
+
"""Return True when the layout item should be rendered.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
expr: The visible value from the layout item.
|
|
112
|
+
current_values: Current variable values for Jinja resolution.
|
|
113
|
+
executor: Required when expr is a SingleRowBoolProbe.
|
|
114
|
+
context: Human-readable label for error messages (e.g. "layout item 'kpi_a'").
|
|
115
|
+
"""
|
|
116
|
+
return eval_bool_condition(
|
|
117
|
+
expr,
|
|
118
|
+
True,
|
|
119
|
+
current_values,
|
|
120
|
+
executor,
|
|
121
|
+
f"{context} visible",
|
|
122
|
+
)
|