dataface 0.3.2.dev22__py3-none-any.whl → 0.3.2.dev111__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/__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 +240 -106
- dataface/agent_api/init.py +1 -1
- dataface/agent_api/project_session.py +2 -1
- dataface/agent_api/serve.py +115 -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 +20 -9
- dataface/cli/commands/search.py +8 -2
- dataface/cli/commands/serve.py +26 -76
- 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 +2 -19
- dataface/core/__init__.py +73 -28
- dataface/core/{compile/colors.py → colors.py} +20 -5
- dataface/core/compile/compiler.py +97 -34
- 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/migrations.py +112 -9
- 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/markers.py +1 -1
- dataface/core/compile/models/primitives.py +175 -2
- dataface/core/compile/models/source.py +5 -5
- 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/vega_lite/config.py +1 -1
- dataface/core/compile/normalize_charts.py +6 -2
- dataface/core/compile/normalize_queries.py +21 -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/yaml_error_formatter.py +1 -1
- dataface/core/connections.py +10 -3
- dataface/core/dashboard.py +24 -18
- dataface/core/defaults/themes/_base.yaml +49 -39
- 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 +6 -6
- dataface/core/defaults/themes/neon.yaml +15 -8
- dataface/core/defaults/themes/plain.yaml +6 -4
- dataface/core/defaults/themes/stark.yaml +16 -7
- dataface/core/defaults/themes/vivid.yaml +15 -8
- dataface/core/diagnostics/__init__.py +2 -2
- dataface/core/diagnostics/codes_compile.py +13 -31
- dataface/core/diagnostics/codes_execute.py +79 -0
- dataface/core/diagnostics/codes_render.py +87 -28
- 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/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/dbt_jinja.py +65 -10
- dataface/core/execute/executor.py +69 -16
- dataface/core/font_measure.py +28 -0
- dataface/core/fonts.py +8 -5
- dataface/core/inspect/__init__.py +46 -6
- dataface/core/project.py +13 -1
- 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/presentation.py +6 -8
- dataface/core/render/chart/rendering.py +3 -12
- 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/converters/chart.py +16 -6
- dataface/core/render/faces.py +3 -2
- 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 +267 -238
- dataface/core/render/numeral_expr.py +80 -0
- dataface/core/render/warnings/bar_band_width_too_narrow.py +64 -6
- dataface/core/render/warnings/layered_chart_shared_y_axis_scale_mismatch.py +25 -3
- 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 +7 -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/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 +9487 -7978
- 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.dev111.dist-info}/METADATA +1 -1
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/RECORD +185 -177
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/WHEEL +0 -0
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/entry_points.txt +0 -0
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/licenses/LICENSE +0 -0
|
@@ -14,6 +14,21 @@ from dataface.core.compile.models.markers import (
|
|
|
14
14
|
Strategy,
|
|
15
15
|
)
|
|
16
16
|
|
|
17
|
+
_TIME_UNIT_LITERAL = Literal[
|
|
18
|
+
"auto",
|
|
19
|
+
"year",
|
|
20
|
+
"yearquarter",
|
|
21
|
+
"yearmonth",
|
|
22
|
+
"yearweek",
|
|
23
|
+
"yearmonthdate",
|
|
24
|
+
"monthofyear",
|
|
25
|
+
"dayofweek",
|
|
26
|
+
"dayofmonth",
|
|
27
|
+
"dayofyear",
|
|
28
|
+
"hourofday",
|
|
29
|
+
"none",
|
|
30
|
+
]
|
|
31
|
+
|
|
17
32
|
|
|
18
33
|
class AxisLabelOverlapConfig(BaseModel):
|
|
19
34
|
"""Two-bool overlap strategy enablement for x-axis labels.
|
|
@@ -59,7 +74,7 @@ from dataface.core.compile.models.style.theme.board import (
|
|
|
59
74
|
class AxisGridZeroStyle(BaseModel):
|
|
60
75
|
"""Zero-baseline gridline color and width overrides.
|
|
61
76
|
|
|
62
|
-
Nested sub-object under
|
|
77
|
+
Nested sub-object under BaseAxisGridStyle.zero. Quantitative axes only —
|
|
63
78
|
band axes never tick at value=0.
|
|
64
79
|
"""
|
|
65
80
|
|
|
@@ -76,13 +91,18 @@ class AxisGridZeroStyle(BaseModel):
|
|
|
76
91
|
)
|
|
77
92
|
|
|
78
93
|
|
|
79
|
-
class
|
|
94
|
+
class BaseAxisGridStyle(BaseModel):
|
|
95
|
+
"""Grid line style for all axis variants.
|
|
96
|
+
|
|
97
|
+
No zero field — that lives only on MeasureGridStyle (AxisYStyle.grid). A
|
|
98
|
+
zero-baseline gridline only makes sense on the measure (y) axis; putting it
|
|
99
|
+
on the shared base would allow authoring it on x-axes where it is silent.
|
|
100
|
+
"""
|
|
101
|
+
|
|
80
102
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
81
103
|
|
|
82
|
-
# All fields are nullable
|
|
83
|
-
#
|
|
84
|
-
# `axis_quantitative`. The base `axis` slot must have all fields non-None;
|
|
85
|
-
# `_build_resolved_axis` validates this at resolve time.
|
|
104
|
+
# All fields are nullable so the cascade can fill missing values from parent
|
|
105
|
+
# `axis` into `axis_x` / `axis_y` / `axis_quantitative`.
|
|
86
106
|
visible: bool | None = Field(
|
|
87
107
|
default=None, description="Show grid lines; None inherits from parent axis."
|
|
88
108
|
)
|
|
@@ -103,17 +123,32 @@ class AxisGridStyle(BaseModel):
|
|
|
103
123
|
default=None,
|
|
104
124
|
description="Dash pattern for grid lines; None renders a solid line.",
|
|
105
125
|
)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class MeasureGridStyle(BaseAxisGridStyle):
|
|
129
|
+
"""BaseAxisGridStyle + zero-baseline override for the measure (y) axis.
|
|
130
|
+
|
|
131
|
+
Used only by AxisYStyle.grid. The zero sub-block is structurally absent on
|
|
132
|
+
all other axis variants so the model rejects it at validation time.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
136
|
+
|
|
137
|
+
# Zero-baseline sub-block. None means "no override at this level"; the
|
|
138
|
+
# cascade fills individual zero.* fields from the parent axis.
|
|
139
|
+
# SkipInheritSlots(cascade=True): apply_inherit copies the entire zero object
|
|
140
|
+
# when the child axis has zero=None; fills individual None fields when partial.
|
|
110
141
|
zero: Annotated[AxisGridZeroStyle | None, SkipInheritSlots(cascade=True)] = Field(
|
|
111
142
|
default=None,
|
|
112
143
|
description="Zero-baseline gridline style; None inherits from parent axis.",
|
|
113
144
|
)
|
|
114
145
|
|
|
115
146
|
|
|
116
|
-
class
|
|
147
|
+
class AxisLineStyle(BaseModel):
|
|
148
|
+
"""Axis domain/baseline line style. Renamed from ``AxisDomainStyle`` —
|
|
149
|
+
same 3 fields, no shape change; ``domain`` collided too easily with
|
|
150
|
+
scale-domain terminology."""
|
|
151
|
+
|
|
117
152
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
118
153
|
|
|
119
154
|
# Nullable so the cascade can fill missing values from parent `axis`.
|
|
@@ -130,56 +165,11 @@ class AxisDomainStyle(BaseModel):
|
|
|
130
165
|
)
|
|
131
166
|
|
|
132
167
|
|
|
133
|
-
class AxisTicksCadenceValidationMixin(BaseModel):
|
|
134
|
-
"""Authored-input validator for step-anchored tick cadence.
|
|
135
|
-
|
|
136
|
-
Applied only to ``AxisTicksStylePatch`` (the authored overlay), not the
|
|
137
|
-
compiled ``AxisTicksStyle`` — the compiled type is the cascade-merged
|
|
138
|
-
result across theme/chart-type/chart-local layers, and a legitimate
|
|
139
|
-
theme default (e.g. editorial's ``axis_quantitative.ticks.count: 6``)
|
|
140
|
-
can land in the same merged object as an unrelated authored ``interval``
|
|
141
|
-
on a different axis slot without either single authored layer actually
|
|
142
|
-
conflicting. Validating only the as-authored patch catches the real
|
|
143
|
-
mistake (both fields set together in the same authored block) without
|
|
144
|
-
that cross-layer false positive. ``build_patch_model_ext`` only carries
|
|
145
|
-
over validators that live on the patch model's base class (mirrors
|
|
146
|
-
``ScaleDomainValidationMixin`` above).
|
|
147
|
-
|
|
148
|
-
Enforces: ``step`` requires ``interval`` (step alone doesn't name a
|
|
149
|
-
cadence unit), and ``interval``/``count`` are mutually exclusive (two
|
|
150
|
-
different ways to express tick cadence). Both fields absent, or
|
|
151
|
-
``interval`` alone, are always valid.
|
|
152
|
-
"""
|
|
153
|
-
|
|
154
|
-
model_config = ConfigDict(extra="forbid")
|
|
155
|
-
|
|
156
|
-
@model_validator(mode="before")
|
|
157
|
-
@classmethod
|
|
158
|
-
def _validate_cadence(cls, data: object) -> object:
|
|
159
|
-
if not isinstance(data, dict):
|
|
160
|
-
return data
|
|
161
|
-
step = data.get("step")
|
|
162
|
-
interval = data.get("interval")
|
|
163
|
-
count = data.get("count")
|
|
164
|
-
if step is not None and interval is None:
|
|
165
|
-
raise ValueError(
|
|
166
|
-
"ticks.step is set without ticks.interval — step alone "
|
|
167
|
-
"doesn't name a cadence unit (year/quarter/month). Author "
|
|
168
|
-
"both (e.g. interval: year, step: 5), or use ticks.count for "
|
|
169
|
-
"VL's advisory tick-count hint instead."
|
|
170
|
-
)
|
|
171
|
-
if interval is not None and count is not None:
|
|
172
|
-
raise ValueError(
|
|
173
|
-
"ticks.interval and ticks.count cannot both be set — they "
|
|
174
|
-
"are two different ways to express tick cadence. Use "
|
|
175
|
-
"ticks.interval/step for a step-anchored cadence (every N "
|
|
176
|
-
"years/quarters/months), or ticks.count for VL's advisory "
|
|
177
|
-
"tick-count hint, not both."
|
|
178
|
-
)
|
|
179
|
-
return data
|
|
180
|
-
|
|
181
|
-
|
|
182
168
|
class AxisTicksStyle(BaseModel):
|
|
169
|
+
"""Universal tick config. ``step``-anchored cadence needs a named time
|
|
170
|
+
unit to mean anything — that unit field lives on ``DimensionTicksStyle``
|
|
171
|
+
(axis_x only), not here; ``step`` alone on this base class is inert."""
|
|
172
|
+
|
|
183
173
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
184
174
|
|
|
185
175
|
# Nullable so the cascade can fill `visible` from parent `axis`.
|
|
@@ -189,7 +179,7 @@ class AxisTicksStyle(BaseModel):
|
|
|
189
179
|
color: str | None = Field(
|
|
190
180
|
default=None, description="Tick color; None uses Vega-Lite's default."
|
|
191
181
|
)
|
|
192
|
-
|
|
182
|
+
length: float | None = Field(
|
|
193
183
|
default=None,
|
|
194
184
|
description="Tick length in pixels; None uses Vega-Lite's default.",
|
|
195
185
|
)
|
|
@@ -197,6 +187,10 @@ class AxisTicksStyle(BaseModel):
|
|
|
197
187
|
default=None,
|
|
198
188
|
description="Tick stroke width in pixels; None uses Vega-Lite's default.",
|
|
199
189
|
)
|
|
190
|
+
offset: float | None = Field(
|
|
191
|
+
default=None,
|
|
192
|
+
description="Pixel offset of ticks from their default position; None means no offset.",
|
|
193
|
+
)
|
|
200
194
|
# SkipInheritSlots: count is not inherited — axis family variants each set their own count.
|
|
201
195
|
count: Annotated[int | None, SkipInheritSlots()] = Field(
|
|
202
196
|
default=None,
|
|
@@ -208,31 +202,99 @@ class AxisTicksStyle(BaseModel):
|
|
|
208
202
|
"which VL's time-scale tick logic honours as a target."
|
|
209
203
|
),
|
|
210
204
|
)
|
|
211
|
-
# SkipInheritSlots:
|
|
212
|
-
|
|
213
|
-
Literal["year", "quarter", "month"] | None, SkipInheritSlots()
|
|
214
|
-
] = Field(
|
|
205
|
+
# SkipInheritSlots: step is not inherited — axis family variants each set their own.
|
|
206
|
+
step: Annotated[int | None, SkipInheritSlots()] = Field(
|
|
215
207
|
default=None,
|
|
216
208
|
description=(
|
|
217
|
-
"Step-anchored tick cadence
|
|
218
|
-
"
|
|
219
|
-
"
|
|
220
|
-
"
|
|
221
|
-
"non-temporal (ordinal) scale."
|
|
209
|
+
"Step multiple for a step-anchored tick cadence (e.g. on "
|
|
210
|
+
"DimensionTicksStyle: time_unit: year, step: 5 -> ticks every 5 "
|
|
211
|
+
"years). Inert on this base class alone — a channel that wants "
|
|
212
|
+
"step-anchored cadence adds the unit field (DimensionTicksStyle.time_unit)."
|
|
222
213
|
),
|
|
223
214
|
)
|
|
224
|
-
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class DimensionTicksStyle(AxisTicksStyle):
|
|
218
|
+
"""axis_x-only: step-anchored tick cadence needs a named time unit.
|
|
219
|
+
|
|
220
|
+
Distinct from ``DimensionLabelStyle.time_unit`` (label-cadence override)
|
|
221
|
+
and ``AxisXStyle.time_unit`` (bucketing grain) — three different
|
|
222
|
+
time_unit fields, three different meanings.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
226
|
+
|
|
227
|
+
time_unit: _TIME_UNIT_LITERAL | None = Field(
|
|
228
|
+
default=None,
|
|
229
|
+
description="Step-anchored tick cadence unit; None disables step-anchored ticks.",
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
@model_validator(mode="after")
|
|
233
|
+
def _validate_cadence(self) -> DimensionTicksStyle:
|
|
234
|
+
if self.step is not None and self.time_unit is None:
|
|
235
|
+
raise ValueError(
|
|
236
|
+
"ticks.step is set without ticks.time_unit — step alone "
|
|
237
|
+
"doesn't name a cadence unit (year/quarter/month/...). "
|
|
238
|
+
"Author both (e.g. time_unit: year, step: 5), or use "
|
|
239
|
+
"ticks.count for VL's advisory tick-count hint instead."
|
|
240
|
+
)
|
|
241
|
+
if self.time_unit is not None and self.count is not None:
|
|
242
|
+
raise ValueError(
|
|
243
|
+
"ticks.time_unit and ticks.count cannot both be set — they "
|
|
244
|
+
"are two different ways to express tick cadence. Use "
|
|
245
|
+
"ticks.time_unit/step for a step-anchored cadence (every N "
|
|
246
|
+
"years/quarters/months), or ticks.count for VL's advisory "
|
|
247
|
+
"tick-count hint, not both."
|
|
248
|
+
)
|
|
249
|
+
return self
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class AxisTitleStyle(BaseModel):
|
|
253
|
+
"""Axis title typography — deliberately thin. A title is one short static
|
|
254
|
+
string, not a per-tick label stream, so it doesn't need the label-overlap
|
|
255
|
+
strategy machinery or VL passthrough knobs (bound/flush/offset/line_height)
|
|
256
|
+
that only make sense for a repeated per-tick label. padding is kept
|
|
257
|
+
(unlike those) since the theme authors a real, non-VL-default value."""
|
|
258
|
+
|
|
259
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
260
|
+
|
|
261
|
+
font: Annotated[FontStyle, InheritSlot(from_path="Style.charts.font")] = Field(
|
|
262
|
+
default_factory=FontStyle, description="Axis title font style overrides."
|
|
263
|
+
)
|
|
264
|
+
padding: float | None = Field(
|
|
265
|
+
default=None,
|
|
266
|
+
description="Padding between axis title and labels in pixels; None uses Vega-Lite's default.",
|
|
267
|
+
)
|
|
268
|
+
angle: float | None = Field(
|
|
269
|
+
default=None,
|
|
270
|
+
description="Title rotation angle in degrees; None uses Vega-Lite's default.",
|
|
271
|
+
)
|
|
272
|
+
# Same left/right/center/inward/outward vocabulary as AxisLabelStyle.align
|
|
273
|
+
# — resolved through the identical _resolve_own_side_align machinery.
|
|
274
|
+
align: Literal["left", "right", "center", "inward", "outward"] | None = Field(
|
|
225
275
|
default=None,
|
|
226
276
|
description=(
|
|
227
|
-
"
|
|
228
|
-
"
|
|
229
|
-
"
|
|
277
|
+
"Horizontal text alignment of the title. 'left'/'right'/'center' "
|
|
278
|
+
"are absolute. 'inward'/'outward' resolve against the axis's own "
|
|
279
|
+
"left/right edge, same as label.align; a no-op on an axis with no "
|
|
280
|
+
"left/right edge. None uses Vega-Lite's default."
|
|
230
281
|
),
|
|
231
282
|
)
|
|
283
|
+
visible: bool | None = Field(
|
|
284
|
+
default=None,
|
|
285
|
+
description="Show the axis title; None uses Vega-Lite's default (title shown).",
|
|
286
|
+
)
|
|
232
287
|
|
|
233
288
|
|
|
234
|
-
class
|
|
235
|
-
"""Axis label
|
|
289
|
+
class AxisLabelStyle(BaseModel):
|
|
290
|
+
"""Axis label: font + padding + VL-passthrough.
|
|
291
|
+
|
|
292
|
+
This is the universal base — fields that are meaningful on both dimension
|
|
293
|
+
and measure axes. Dimension-only fields (tilt_increments, values,
|
|
294
|
+
time_unit) live on DimensionLabelStyle, which is used for AxisXStyle.labels.
|
|
295
|
+
expr stays here: it is universal and read by time_unit_detect.py on both
|
|
296
|
+
axes.
|
|
297
|
+
"""
|
|
236
298
|
|
|
237
299
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
238
300
|
|
|
@@ -268,10 +330,6 @@ class AxisElementStyle(BaseModel):
|
|
|
268
330
|
"default."
|
|
269
331
|
),
|
|
270
332
|
)
|
|
271
|
-
baseline: str | None = Field(
|
|
272
|
-
default=None,
|
|
273
|
-
description="Vertical text baseline of labels; None uses Vega-Lite's default.",
|
|
274
|
-
)
|
|
275
333
|
# Two-bool struct enabling/disabling each overlap strategy.
|
|
276
334
|
# None = not authored — inherit the parent axis's setting from the cascade.
|
|
277
335
|
# SkipInheritSlots(cascade=True): if overlap is None on the child axis,
|
|
@@ -290,38 +348,18 @@ class AxisElementStyle(BaseModel):
|
|
|
290
348
|
"to a coarser calendar period without changing the axis or label time unit."
|
|
291
349
|
),
|
|
292
350
|
)
|
|
293
|
-
|
|
351
|
+
min_gap: float | None = Field(
|
|
294
352
|
default=None,
|
|
295
|
-
description="Minimum pixel
|
|
353
|
+
description="Minimum pixel gap between labels; None uses Vega-Lite's default (0px).",
|
|
296
354
|
)
|
|
297
355
|
visible: bool | None = Field(
|
|
298
356
|
default=None,
|
|
299
357
|
description="Show axis labels; None uses Vega-Lite's default (labels shown).",
|
|
300
358
|
)
|
|
301
|
-
# Optional label cadence for temporal axes. None inherits from the parent
|
|
302
|
-
# axis time_unit; render-time layout may promote sub-month auto labels.
|
|
303
|
-
time_unit: (
|
|
304
|
-
Literal[
|
|
305
|
-
"auto",
|
|
306
|
-
"year",
|
|
307
|
-
"yearquarter",
|
|
308
|
-
"yearmonth",
|
|
309
|
-
"yearweek",
|
|
310
|
-
"yearmonthdate",
|
|
311
|
-
"monthofyear",
|
|
312
|
-
"dayofweek",
|
|
313
|
-
"dayofmonth",
|
|
314
|
-
"dayofyear",
|
|
315
|
-
"hourofday",
|
|
316
|
-
"none",
|
|
317
|
-
]
|
|
318
|
-
| None
|
|
319
|
-
) = Field(
|
|
320
|
-
default=None,
|
|
321
|
-
description="Label cadence for temporal axes; None inherits from the parent axis time_unit.",
|
|
322
|
-
)
|
|
323
359
|
# When None and the parent axis has a temporal time_unit, the render layer
|
|
324
360
|
# fills in a smart conditional labelExpr — see render/chart/time_unit_detect.py.
|
|
361
|
+
# Kept on the base (not just DimensionLabelStyle) — both axes may use custom
|
|
362
|
+
# label expressions, e.g. for axis_y mirror overrides via AxisMirrorStyle.expr.
|
|
325
363
|
expr: str | None = Field(
|
|
326
364
|
default=None,
|
|
327
365
|
description="Custom Vega expression for label text; None uses smart temporal defaults when applicable.",
|
|
@@ -339,11 +377,27 @@ class AxisElementStyle(BaseModel):
|
|
|
339
377
|
default=None,
|
|
340
378
|
description="Pixel offset of the label from its tick anchor; None uses Vega-Lite's default.",
|
|
341
379
|
)
|
|
342
|
-
|
|
380
|
+
format: str | None = Field(
|
|
381
|
+
default=None, description="Tick value format string; None uses auto-format."
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class DimensionLabelStyle(AxisLabelStyle):
|
|
386
|
+
"""AxisLabelStyle + dimension-axis-only label fields.
|
|
387
|
+
|
|
388
|
+
Used only by AxisXStyle.labels. Fields here are structurally absent on
|
|
389
|
+
AxisYStyle.labels so the model rejects them at validation time rather than
|
|
390
|
+
requiring a scattered runtime check.
|
|
391
|
+
"""
|
|
392
|
+
|
|
393
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
394
|
+
|
|
395
|
+
# Optional label cadence for temporal axes. None inherits from the parent
|
|
396
|
+
# axis time_unit, except yearweek defaults to month labels.
|
|
397
|
+
time_unit: _TIME_UNIT_LITERAL | None = Field(
|
|
343
398
|
default=None,
|
|
344
|
-
description="
|
|
399
|
+
description="Label cadence for temporal axes; None inherits from the parent axis time_unit.",
|
|
345
400
|
)
|
|
346
|
-
anchor: Literal["start", "middle", "end"] | None = Field(default=None, description="Label anchor position; None uses Vega-Lite's default.") # fmt: skip
|
|
347
401
|
# Descending tilt ladder consulted by the "tilt" strategy on discrete x-axes.
|
|
348
402
|
# The picker walks this list and returns the first angle whose label widths
|
|
349
403
|
# fit at the chart's drawable width; fall-through uses the last entry (steepest).
|
|
@@ -356,15 +410,17 @@ class AxisElementStyle(BaseModel):
|
|
|
356
410
|
# Decouples LABEL density from tick/grid density on temporal x-axes. This
|
|
357
411
|
# is a filter, not a tick list: it does not add or remove ticks. Ticks and
|
|
358
412
|
# gridlines are produced exactly as they would be without this field —
|
|
359
|
-
# from ``
|
|
360
|
-
# of those ticks then gets its label text kept only if its date
|
|
361
|
-
# in this list; every other tick keeps its position/gridline but
|
|
362
|
-
# label is blanked. A date here that doesn't match any actual tick is
|
|
363
|
-
# silent no-op (nothing exists to unblank). To change tick/grid density
|
|
364
|
-
# itself, author ``axis_x.values``. None labels every tick per the
|
|
413
|
+
# from ``axis_x.scale.values`` if authored, else the auto-fill default.
|
|
414
|
+
# Each of those ticks then gets its label text kept only if its date
|
|
415
|
+
# appears in this list; every other tick keeps its position/gridline but
|
|
416
|
+
# its label is blanked. A date here that doesn't match any actual tick is
|
|
417
|
+
# a silent no-op (nothing exists to unblank). To change tick/grid density
|
|
418
|
+
# itself, author ``axis_x.scale.values``. None labels every tick per the
|
|
365
419
|
# existing smart cadence/format. Temporal x-axes only — the render layer
|
|
366
|
-
# raises when authored on a non-temporal axis
|
|
367
|
-
# axis is never temporal in Dataface's model)
|
|
420
|
+
# raises when authored on a non-temporal axis, on axis_y (the measure
|
|
421
|
+
# axis is never temporal in Dataface's model), or on a bar chart whose
|
|
422
|
+
# categorical axis renders horizontal (that axis is always nominal,
|
|
423
|
+
# regardless of orientation being forced or auto-inferred).
|
|
368
424
|
values: Annotated[list[Any] | None, Merge(Strategy.OVERRIDE)] = Field(
|
|
369
425
|
default=None,
|
|
370
426
|
min_length=1,
|
|
@@ -375,7 +431,8 @@ class AxisElementStyle(BaseModel):
|
|
|
375
431
|
"add or remove ticks. Every tick not in this list keeps its "
|
|
376
432
|
"position and gridline but has its label blanked. Set "
|
|
377
433
|
"axis_x.values separately to change tick/grid density itself. "
|
|
378
|
-
"Temporal x-axes only;
|
|
434
|
+
"Temporal x-axes only; not supported on a horizontal bar's "
|
|
435
|
+
"categorical axis. None labels every tick as usual."
|
|
379
436
|
),
|
|
380
437
|
)
|
|
381
438
|
|
|
@@ -407,7 +464,7 @@ def _is_iso_date_string(value: str) -> bool:
|
|
|
407
464
|
class ScaleDomainValidationMixin(BaseModel):
|
|
408
465
|
"""Shared authored-input validator for scale domain shape/consistency.
|
|
409
466
|
|
|
410
|
-
Inherited by both ``
|
|
467
|
+
Inherited by both ``BaseScaleStyle`` and ``BaseScaleStylePatch`` so chart-local
|
|
411
468
|
authored patches enforce the same domain rules as theme-stage input —
|
|
412
469
|
``build_patch_model_ext`` only carries over validators that live on the
|
|
413
470
|
patch model's base class (mirrors ``_FaceDesugarMixin``).
|
|
@@ -447,86 +504,63 @@ class ScaleDomainValidationMixin(BaseModel):
|
|
|
447
504
|
return data
|
|
448
505
|
|
|
449
506
|
|
|
450
|
-
class
|
|
451
|
-
"""
|
|
507
|
+
class ScaleLogStyle(BaseModel):
|
|
508
|
+
"""Log-scale-only param."""
|
|
452
509
|
|
|
453
510
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
454
511
|
|
|
455
|
-
|
|
456
|
-
default=None,
|
|
457
|
-
description="Minimum band mark width in pixels; None uses Vega-Lite's default.",
|
|
458
|
-
)
|
|
459
|
-
max_size: float | None = Field( # None = no max constraint; authored overlay
|
|
512
|
+
base: float | None = Field(
|
|
460
513
|
default=None,
|
|
461
|
-
description="
|
|
514
|
+
description="Log base; only meaningful with type: log. None lets Vega-Lite apply its own default (10).",
|
|
462
515
|
)
|
|
463
516
|
|
|
464
517
|
|
|
465
|
-
class
|
|
466
|
-
"""
|
|
467
|
-
|
|
468
|
-
Used both at chart-level (``style.scale`` — applies to both x and y) and
|
|
469
|
-
per-axis (``style.axis_x.scale``, ``style.axis_y.scale`` — overrides on
|
|
470
|
-
that channel only). Per-axis values win on conflict.
|
|
471
|
-
"""
|
|
518
|
+
class ScalePowStyle(BaseModel):
|
|
519
|
+
"""Power-scale-only param."""
|
|
472
520
|
|
|
473
521
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
474
522
|
|
|
475
|
-
|
|
476
|
-
default=None,
|
|
477
|
-
description="Inner padding fraction for band scales; None uses Vega-Lite's default.",
|
|
478
|
-
)
|
|
479
|
-
band_padding_outer: float | None = Field(
|
|
480
|
-
default=None,
|
|
481
|
-
description="Outer padding fraction for band scales; None uses Vega-Lite's default.",
|
|
482
|
-
)
|
|
483
|
-
point_padding: float | None = Field(
|
|
484
|
-
default=None,
|
|
485
|
-
description="Padding fraction for point scales; None uses Vega-Lite's default.",
|
|
486
|
-
)
|
|
487
|
-
rect_band_padding_inner: float | None = Field(
|
|
523
|
+
exponent: float | None = Field(
|
|
488
524
|
default=None,
|
|
489
|
-
description="
|
|
525
|
+
description="Power exponent; only meaningful with type: pow. None uses Vega-Lite's default.",
|
|
490
526
|
)
|
|
491
|
-
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class ScaleSymlogStyle(BaseModel):
|
|
530
|
+
"""Symlog-scale-only param."""
|
|
531
|
+
|
|
532
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
533
|
+
|
|
534
|
+
constant: float | None = Field(
|
|
492
535
|
default=None,
|
|
493
|
-
description="
|
|
536
|
+
description="Symlog constant; only meaningful with type: symlog. None uses Vega-Lite's default.",
|
|
494
537
|
)
|
|
495
|
-
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
class ScaleContinuousStyle(ScaleDomainValidationMixin):
|
|
541
|
+
"""Continuous-scale-only config: type, domain, zero-baseline, log/pow/symlog params.
|
|
542
|
+
|
|
543
|
+
Carries two validators moved from the old flat ``BaseScaleStyle``:
|
|
544
|
+
- ScaleDomainValidationMixin: the domain-shape check (applies to both this
|
|
545
|
+
class and its generated Patch via build_patch_model_ext).
|
|
546
|
+
- _validate_log_pow_symlog_params: defined DIRECTLY here, NOT on a mixin —
|
|
547
|
+
this validator fires on the fully-merged result; it must NOT carry into
|
|
548
|
+
the generated Patch since a theme layer may set type: log and a
|
|
549
|
+
chart-local patch layer may set only base. The cross-field check fires
|
|
550
|
+
once, on the merged result.
|
|
551
|
+
|
|
552
|
+
All fields optional — see test_scale_groups_have_no_required_fields.
|
|
553
|
+
"""
|
|
554
|
+
|
|
555
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
556
|
+
|
|
557
|
+
zero: bool | Literal["auto"] | None = Field(
|
|
496
558
|
default=None,
|
|
497
559
|
description=(
|
|
498
560
|
'Force scale zero-baseline: True/False pins it; "auto" runs the '
|
|
499
561
|
"Dataface smart-zero heuristic; None passes through to Vega-Lite."
|
|
500
562
|
),
|
|
501
563
|
)
|
|
502
|
-
clamp: bool | None = Field(
|
|
503
|
-
default=None,
|
|
504
|
-
description="Clamp values to scale domain; None uses Vega-Lite's default (no clamping).",
|
|
505
|
-
)
|
|
506
|
-
continuous_padding: float | None = Field(
|
|
507
|
-
default=None,
|
|
508
|
-
description="Padding in pixels for continuous scales; None uses Vega-Lite's default.",
|
|
509
|
-
)
|
|
510
|
-
band: ScaleBandStyle | None = Field( # None = no band size constraint authored
|
|
511
|
-
default=None,
|
|
512
|
-
description="Band mark size constraints; None uses Vega-Lite's defaults.",
|
|
513
|
-
)
|
|
514
|
-
quantile_count: int | None = Field(
|
|
515
|
-
default=None,
|
|
516
|
-
description="Number of quantile buckets; None uses Vega-Lite's default.",
|
|
517
|
-
)
|
|
518
|
-
quantize_count: int | None = Field(
|
|
519
|
-
default=None,
|
|
520
|
-
description="Number of quantize buckets; None uses Vega-Lite's default.",
|
|
521
|
-
)
|
|
522
|
-
use_unaggregated_domain: bool | None = Field(
|
|
523
|
-
default=None,
|
|
524
|
-
description="Use unaggregated domain for scale extent; None uses Vega-Lite's default.",
|
|
525
|
-
)
|
|
526
|
-
x_reverse: bool | None = Field(
|
|
527
|
-
default=None,
|
|
528
|
-
description="Reverse the x-axis scale direction; None means no reversal.",
|
|
529
|
-
)
|
|
530
564
|
type: Literal["linear", "log", "pow", "sqrt", "symlog", "temporal"] | None = Field(
|
|
531
565
|
default=None,
|
|
532
566
|
description=(
|
|
@@ -551,21 +585,6 @@ class ScaleStyle(ScaleDomainValidationMixin):
|
|
|
551
585
|
"date/datetime strings."
|
|
552
586
|
),
|
|
553
587
|
)
|
|
554
|
-
base: float | None = Field(
|
|
555
|
-
default=None,
|
|
556
|
-
description=(
|
|
557
|
-
"Log base; only meaningful with type: log. None lets Vega-Lite "
|
|
558
|
-
"apply its own default (10)."
|
|
559
|
-
),
|
|
560
|
-
)
|
|
561
|
-
exponent: float | None = Field(
|
|
562
|
-
default=None,
|
|
563
|
-
description="Power exponent; only meaningful with type: pow. None uses Vega-Lite's default.",
|
|
564
|
-
)
|
|
565
|
-
constant: float | None = Field(
|
|
566
|
-
default=None,
|
|
567
|
-
description="Symlog constant; only meaningful with type: symlog. None uses Vega-Lite's default.",
|
|
568
|
-
)
|
|
569
588
|
domain_strategy: Literal["stable", "auto"] | None = Field(
|
|
570
589
|
default=None,
|
|
571
590
|
description=(
|
|
@@ -574,10 +593,82 @@ class ScaleStyle(ScaleDomainValidationMixin):
|
|
|
574
593
|
"the domain to the chart query's full unfiltered extent — the same "
|
|
575
594
|
"value lands at the same pixel across variable changes. 'auto' "
|
|
576
595
|
"rescales the domain to the current filtered subset on every variable "
|
|
577
|
-
"change. An explicit `domain` always wins over "
|
|
578
|
-
"either strategy."
|
|
596
|
+
"change. An explicit `domain` always wins over either strategy."
|
|
579
597
|
),
|
|
580
598
|
)
|
|
599
|
+
log: ScaleLogStyle | None = Field(
|
|
600
|
+
default=None,
|
|
601
|
+
description="Log-scale param (base); only meaningful with type: log.",
|
|
602
|
+
)
|
|
603
|
+
pow: ScalePowStyle | None = Field(
|
|
604
|
+
default=None,
|
|
605
|
+
description="Power-scale param (exponent); only meaningful with type: pow.",
|
|
606
|
+
)
|
|
607
|
+
symlog: ScaleSymlogStyle | None = Field(
|
|
608
|
+
default=None,
|
|
609
|
+
description="Symlog-scale param (constant); only meaningful with type: symlog.",
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
# NOT on ScaleDomainValidationMixin: unlike domain shape, this pairing can
|
|
613
|
+
# legitimately span two cascade layers (a theme sets type: log, a chart
|
|
614
|
+
# overlay sets only log.base) — merge_onto_base re-validates the fully-merged
|
|
615
|
+
# ScaleContinuousStyle via model_validate, so this fires on the merged
|
|
616
|
+
# result, not on an isolated authored patch fragment (the generated Patch
|
|
617
|
+
# does not inherit it — it is defined directly here, not on a mixin).
|
|
618
|
+
@model_validator(mode="after")
|
|
619
|
+
def _validate_log_pow_symlog_params(self) -> ScaleContinuousStyle:
|
|
620
|
+
if self.log is not None and self.log.base is not None and self.type != "log":
|
|
621
|
+
raise ValueError(
|
|
622
|
+
"scale.continuous.log.base is only meaningful with type: log — "
|
|
623
|
+
"set axis_y.scale.continuous.type: log, or remove log.base."
|
|
624
|
+
)
|
|
625
|
+
if (
|
|
626
|
+
self.pow is not None
|
|
627
|
+
and self.pow.exponent is not None
|
|
628
|
+
and self.type != "pow"
|
|
629
|
+
):
|
|
630
|
+
raise ValueError(
|
|
631
|
+
"scale.continuous.pow.exponent is only meaningful with type: pow "
|
|
632
|
+
"— set axis_y.scale.continuous.type: pow, or remove pow.exponent."
|
|
633
|
+
)
|
|
634
|
+
if (
|
|
635
|
+
self.symlog is not None
|
|
636
|
+
and self.symlog.constant is not None
|
|
637
|
+
and self.type != "symlog"
|
|
638
|
+
):
|
|
639
|
+
raise ValueError(
|
|
640
|
+
"scale.continuous.symlog.constant is only meaningful with type: "
|
|
641
|
+
"symlog — set axis_y.scale.continuous.type: symlog, or remove "
|
|
642
|
+
"symlog.constant."
|
|
643
|
+
)
|
|
644
|
+
if self.type == "log" and self.zero is True:
|
|
645
|
+
raise ValueError(
|
|
646
|
+
"scale.continuous.type: log is incompatible with zero: true — a log "
|
|
647
|
+
"domain cannot include 0. Remove zero or drop the log scale."
|
|
648
|
+
)
|
|
649
|
+
return self
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
class BaseScaleStyle(BaseModel):
|
|
653
|
+
"""Universal + continuous-only scale config.
|
|
654
|
+
|
|
655
|
+
Channel-agnostic — used at chart-level ``scale:`` (applies to both x and y)
|
|
656
|
+
and at ``axis_y.scale``. band/point/quantize/mark_size scale config was
|
|
657
|
+
deleted (2026-08 trim): no chart currently needs it, and it more than
|
|
658
|
+
doubled the authoring surface for zero real use. Rely on Vega-Lite's own
|
|
659
|
+
defaults until a real need for those knobs shows up.
|
|
660
|
+
"""
|
|
661
|
+
|
|
662
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
663
|
+
|
|
664
|
+
round: bool | None = Field(
|
|
665
|
+
default=None,
|
|
666
|
+
description="Round scale outputs to nearest integer; None uses Vega-Lite's default (no rounding).",
|
|
667
|
+
)
|
|
668
|
+
clamp: bool | None = Field(
|
|
669
|
+
default=None,
|
|
670
|
+
description="Clamp values to scale domain; None uses Vega-Lite's default (no clamping).",
|
|
671
|
+
)
|
|
581
672
|
nice: bool | None = Field(
|
|
582
673
|
default=None,
|
|
583
674
|
description="Round scale domain to nice values; None uses Vega-Lite's default.",
|
|
@@ -604,36 +695,34 @@ class ScaleStyle(ScaleDomainValidationMixin):
|
|
|
604
695
|
"Ignored with an explicit `domain` or `stack: normalize`."
|
|
605
696
|
),
|
|
606
697
|
)
|
|
698
|
+
# Explicit tick values for the scale (not to be confused with
|
|
699
|
+
# labels.values, which is a dimension-only label density filter on
|
|
700
|
+
# DimensionLabelStyle). Moved in from the old BaseAxisStyle.values.
|
|
701
|
+
values: Annotated[list[Any] | None, Merge(Strategy.OVERRIDE)] = Field(
|
|
702
|
+
default=None,
|
|
703
|
+
description="Explicit tick values; None uses Vega-Lite's auto tick values.",
|
|
704
|
+
)
|
|
705
|
+
continuous: ScaleContinuousStyle | None = Field(
|
|
706
|
+
default=None,
|
|
707
|
+
description="Continuous-scale overrides (type, domain, zero, log/pow/symlog params); None means no override.",
|
|
708
|
+
)
|
|
607
709
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
"axis_y.scale.type: pow, or remove exponent."
|
|
625
|
-
)
|
|
626
|
-
if self.constant is not None and self.type != "symlog":
|
|
627
|
-
raise ValueError(
|
|
628
|
-
"scale.constant is only meaningful with type: symlog — set "
|
|
629
|
-
"axis_y.scale.type: symlog, or remove constant."
|
|
630
|
-
)
|
|
631
|
-
if self.type == "log" and self.zero is True:
|
|
632
|
-
raise ValueError(
|
|
633
|
-
"scale.type: log is incompatible with zero: true — a log "
|
|
634
|
-
"domain cannot include 0. Remove zero or drop the log scale."
|
|
635
|
-
)
|
|
636
|
-
return self
|
|
710
|
+
|
|
711
|
+
class XScaleStyle(BaseScaleStyle):
|
|
712
|
+
"""Scale config for axis_x.scale only.
|
|
713
|
+
|
|
714
|
+
x_reverse is structurally absent from BaseScaleStyle so the model rejects
|
|
715
|
+
it on axis_y.scale and chart-level scale: at validation time, closing the
|
|
716
|
+
silent-leak bug where x_reverse on a chart-level scale reached VL's y-scale
|
|
717
|
+
via the old flat SCALE_FIELD_MAP.
|
|
718
|
+
"""
|
|
719
|
+
|
|
720
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
721
|
+
|
|
722
|
+
x_reverse: bool | None = Field(
|
|
723
|
+
default=None,
|
|
724
|
+
description="Reverse the x-axis scale direction; None means no reversal.",
|
|
725
|
+
)
|
|
637
726
|
|
|
638
727
|
|
|
639
728
|
class TooltipSlotStyle(BaseModel):
|
|
@@ -784,107 +873,77 @@ class AxisMirrorStyle(BaseModel):
|
|
|
784
873
|
return self
|
|
785
874
|
|
|
786
875
|
|
|
787
|
-
class
|
|
876
|
+
class BaseAxisStyle(BaseModel):
|
|
877
|
+
"""Universal axis surface — grid/line/ticks/labels/title/scale.
|
|
878
|
+
|
|
879
|
+
Channel-agnostic: used for the theme's ``axis:`` slot (applies to all axes)
|
|
880
|
+
and for AxisXStyle/AxisYStyle/QuantitativeAxisStyle/BandAxisStyle subclasses.
|
|
881
|
+
Dimension-only and measure-only fields live on the appropriate subclass so
|
|
882
|
+
the model rejects them at validation time rather than requiring scattered
|
|
883
|
+
runtime checks. No ``position`` here — AxisXStyle/AxisYStyle each declare
|
|
884
|
+
their own narrowed literal; axis_quantitative/axis_band have no position
|
|
885
|
+
concept of their own.
|
|
886
|
+
"""
|
|
887
|
+
|
|
788
888
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
789
889
|
|
|
790
|
-
grid:
|
|
791
|
-
default_factory=
|
|
890
|
+
grid: BaseAxisGridStyle = Field(
|
|
891
|
+
default_factory=BaseAxisGridStyle,
|
|
892
|
+
description="Grid line style for this axis.",
|
|
792
893
|
)
|
|
793
|
-
|
|
794
|
-
default_factory=
|
|
894
|
+
line: AxisLineStyle = Field(
|
|
895
|
+
default_factory=AxisLineStyle, description="Domain line style for this axis."
|
|
795
896
|
)
|
|
796
897
|
ticks: AxisTicksStyle = Field(
|
|
797
898
|
default_factory=AxisTicksStyle, description="Tick mark style for this axis."
|
|
798
899
|
)
|
|
799
|
-
|
|
800
|
-
default_factory=
|
|
900
|
+
labels: AxisLabelStyle = Field(
|
|
901
|
+
default_factory=AxisLabelStyle, description="Axis label style."
|
|
801
902
|
)
|
|
802
|
-
title:
|
|
803
|
-
default_factory=
|
|
903
|
+
title: AxisTitleStyle = Field(
|
|
904
|
+
default_factory=AxisTitleStyle, description="Axis title style."
|
|
804
905
|
)
|
|
805
|
-
#
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
# via the axis_x/y/quantitative slot from adding incorrect graph entries.
|
|
809
|
-
|
|
810
|
-
# "auto" → "right" by default; flips to "left" when chart has right-edge
|
|
811
|
-
# series labels (e.g. line endpoint labels). VL passthrough for explicit values.
|
|
812
|
-
position: Annotated[
|
|
813
|
-
Literal["left", "right", "top", "bottom", "auto"] | None, SkipInheritSlots()
|
|
814
|
-
] = Field(
|
|
815
|
-
default=None,
|
|
816
|
-
description="Axis position; auto flips y-axis when endpoint labels are present.",
|
|
817
|
-
)
|
|
818
|
-
# Only populated on axis_y in theme YAML; None on shared/x/quantitative/band
|
|
819
|
-
# axes means "skip this VL property" matching the VL config emit layer.
|
|
820
|
-
categorical_orient: Annotated[str | None, SkipInheritSlots()] = Field(
|
|
821
|
-
default=None,
|
|
822
|
-
description="Orientation for categorical y-axis (e.g. 'left'); None skips this VL property.",
|
|
906
|
+
# SkipInheritSlots: scale is not inherited — axis family variants each own their scale config.
|
|
907
|
+
scale: Annotated[BaseScaleStyle | None, SkipInheritSlots()] = Field(
|
|
908
|
+
default=None, description="Per-axis scale overrides; None means no override."
|
|
823
909
|
)
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
class AxisXStyle(BaseAxisStyle):
|
|
913
|
+
"""Dimension/category-time axis style. Theme slot: axis_x.
|
|
914
|
+
|
|
915
|
+
Adds dimension-only fields: fill (required — theme supplies via axis_x.fill),
|
|
916
|
+
time_unit (bucketing), type (ordinal/temporal discriminant), and overrides
|
|
917
|
+
ticks/labels with DimensionTicksStyle/DimensionLabelStyle.
|
|
918
|
+
"""
|
|
919
|
+
|
|
920
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
921
|
+
|
|
922
|
+
# Override base's ticks type to add step-anchored cadence's unit field.
|
|
923
|
+
ticks: DimensionTicksStyle = Field(
|
|
924
|
+
default_factory=DimensionTicksStyle, description="Dimension axis tick style."
|
|
835
925
|
)
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
description="
|
|
926
|
+
# Override base's labels type to add dimension-only label fields.
|
|
927
|
+
labels: DimensionLabelStyle = Field(
|
|
928
|
+
default_factory=DimensionLabelStyle, description="Dimension axis label style."
|
|
839
929
|
)
|
|
840
|
-
|
|
930
|
+
position: Annotated[Literal["top", "bottom"] | None, SkipInheritSlots()] = Field(
|
|
841
931
|
default=None,
|
|
842
|
-
description="
|
|
843
|
-
)
|
|
844
|
-
# Encoding-level passthrough: applied to encoding.y.axis, not config.axis
|
|
845
|
-
format: Annotated[str | None, SkipInheritSlots()] = Field(
|
|
846
|
-
default=None, description="Tick value format string; None uses auto-format."
|
|
932
|
+
description="X-axis position; None uses Vega-Lite's default (bottom).",
|
|
847
933
|
)
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
] = Field(
|
|
934
|
+
# Override base's scale type to XScaleStyle so x_reverse can be authored here.
|
|
935
|
+
scale: Annotated[XScaleStyle | None, SkipInheritSlots()] = Field(
|
|
851
936
|
default=None,
|
|
852
|
-
description="
|
|
937
|
+
description="X-axis scale overrides including x_reverse; None means no override.",
|
|
853
938
|
)
|
|
854
939
|
# Time-unit bucketing for temporal x axes (auto-detected when None or "auto").
|
|
855
940
|
# "none" disables bucketing (continuous temporal). VL timeUnit values otherwise.
|
|
856
|
-
time_unit: Annotated[
|
|
857
|
-
Literal[
|
|
858
|
-
"auto",
|
|
859
|
-
"year",
|
|
860
|
-
"yearquarter",
|
|
861
|
-
"yearmonth",
|
|
862
|
-
"yearweek",
|
|
863
|
-
"yearmonthdate",
|
|
864
|
-
"monthofyear",
|
|
865
|
-
"dayofweek",
|
|
866
|
-
"dayofmonth",
|
|
867
|
-
"dayofyear",
|
|
868
|
-
"hourofday",
|
|
869
|
-
"none",
|
|
870
|
-
]
|
|
871
|
-
| None,
|
|
872
|
-
SkipInheritSlots(),
|
|
873
|
-
] = Field(
|
|
941
|
+
time_unit: Annotated[_TIME_UNIT_LITERAL | None, SkipInheritSlots()] = Field(
|
|
874
942
|
default=None,
|
|
875
943
|
description="Time-unit bucketing for temporal x-axes; None or 'auto' auto-detects from data.",
|
|
876
944
|
)
|
|
877
|
-
#
|
|
878
|
-
#
|
|
879
|
-
# SkipInheritSlots: scale is not inherited — axis family variants each own their scale config.
|
|
880
|
-
scale: Annotated[ScaleStyle | None, SkipInheritSlots()] = Field(
|
|
881
|
-
default=None, description="Per-axis scale overrides; None means no override."
|
|
882
|
-
)
|
|
883
|
-
|
|
884
|
-
# Authored opt-in scale type for bucketed-time x-axes (axis_x only).
|
|
885
|
-
# None/"auto" = ordinal when time_unit resolves to a bucketed-calendar
|
|
886
|
-
# grain, temporal for continuous date. "temporal" forces the temporal
|
|
887
|
-
# escape-hatch path. "ordinal" forces ordinal. Author always wins.
|
|
945
|
+
# Authored opt-in scale type for bucketed-time x-axes.
|
|
946
|
+
# None/"auto" = ordinal when time_unit resolves to a bucketed-calendar grain.
|
|
888
947
|
type: Annotated[
|
|
889
948
|
Literal["auto", "ordinal", "temporal"] | None, SkipInheritSlots()
|
|
890
949
|
] = Field(
|
|
@@ -892,11 +951,8 @@ class AxisStyle(BaseModel):
|
|
|
892
951
|
description="Scale type for bucketed-time x-axes; None/'auto' infers from time_unit grain.",
|
|
893
952
|
)
|
|
894
953
|
# Fill mode for missing time buckets on ordinal bucketed-time x-axes.
|
|
895
|
-
# "null" → synthesized rows carry null measures; line/area breaks at gap.
|
|
896
|
-
# "zero" → synthesized rows carry 0 measures; event-count charts opt in.
|
|
897
|
-
# linear / step-* / curve → fill interior synthetic buckets (Looker-aligned names).
|
|
898
954
|
# Required; theme YAML supplies the default ("null") via axis_x.fill.
|
|
899
|
-
# SkipInheritSlots: fill is
|
|
955
|
+
# SkipInheritSlots: fill is x-axis-only; always non-None (validator converts None→"null").
|
|
900
956
|
fill: Annotated[
|
|
901
957
|
Literal[
|
|
902
958
|
"null",
|
|
@@ -919,11 +975,11 @@ class AxisStyle(BaseModel):
|
|
|
919
975
|
# 1 (January) is the calendar-quarter convention; any other value shifts
|
|
920
976
|
# both the bucket boundaries (_enumerate_buckets) and the Q1..Q4 numbering
|
|
921
977
|
# / year-boundary detection (is_label_opener, opens_label_period,
|
|
922
|
-
# _quarter_label) by the same offset. Required (like fill)
|
|
923
|
-
#
|
|
924
|
-
# for the year/yearquarter grains — yearmonth/yearweek/
|
|
925
|
-
# bucket boundaries are fixed regardless of this value.
|
|
926
|
-
# SkipInheritSlots:
|
|
978
|
+
# _quarter_label) by the same offset. Required (like fill) — theme YAML
|
|
979
|
+
# supplies the default (1) via axis_x.fiscal_year_start_month. Only
|
|
980
|
+
# meaningful for the year/yearquarter grains — yearmonth/yearweek/
|
|
981
|
+
# yearmonthdate bucket boundaries are fixed regardless of this value.
|
|
982
|
+
# SkipInheritSlots: matches fill/time_unit/type (x-axis-only fields).
|
|
927
983
|
fiscal_year_start_month: Annotated[int, SkipInheritSlots()] = Field(
|
|
928
984
|
description=(
|
|
929
985
|
"Calendar month (1=Jan..12=Dec) that opens a fiscal year/quarter "
|
|
@@ -952,3 +1008,67 @@ class AxisStyle(BaseModel):
|
|
|
952
1008
|
if value is None:
|
|
953
1009
|
return "null"
|
|
954
1010
|
return value
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
class AxisYStyle(BaseAxisStyle):
|
|
1014
|
+
"""Measure axis style. Theme slot: axis_y.
|
|
1015
|
+
|
|
1016
|
+
Adds measure-only fields: mirror; overrides grid with MeasureGridStyle
|
|
1017
|
+
(adds grid.zero). x-axis-only fields (fill, time_unit, type,
|
|
1018
|
+
tilt_increments, labels.values) are structurally absent so the model
|
|
1019
|
+
rejects them at validation time.
|
|
1020
|
+
|
|
1021
|
+
No ``categorical_orient`` (deleted 2026-08 trim) — ``position`` (below)
|
|
1022
|
+
now serves the "which side does this axis's orient read as" role for
|
|
1023
|
+
every consumer, dimension-swap-into-VL-y included.
|
|
1024
|
+
"""
|
|
1025
|
+
|
|
1026
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
1027
|
+
|
|
1028
|
+
# Override base's grid type to add grid.zero (measure axis only).
|
|
1029
|
+
grid: MeasureGridStyle = Field(
|
|
1030
|
+
default_factory=MeasureGridStyle,
|
|
1031
|
+
description="Measure axis grid style (includes zero-baseline override).",
|
|
1032
|
+
)
|
|
1033
|
+
position: Annotated[Literal["left", "right", "auto"] | None, SkipInheritSlots()] = (
|
|
1034
|
+
Field(
|
|
1035
|
+
default=None,
|
|
1036
|
+
description="Y-axis position; auto flips when endpoint labels are on the right.",
|
|
1037
|
+
)
|
|
1038
|
+
)
|
|
1039
|
+
# y-axis only: draw the y-scale on both edges.
|
|
1040
|
+
mirror: Annotated[bool | AxisMirrorStyle | None, SkipInheritSlots()] = Field(
|
|
1041
|
+
default=None,
|
|
1042
|
+
description=(
|
|
1043
|
+
"Draw the y-scale on both left and right edges (wide charts). "
|
|
1044
|
+
"true mirrors the primary axis's label verbatim; an object (format/expr) "
|
|
1045
|
+
"relabels only the mirrored edge — e.g. a percent-of-total right axis "
|
|
1046
|
+
"next to an absolute-value left axis — while ticks stay aligned to the "
|
|
1047
|
+
"single shared scale. Only meaningful on axis_y."
|
|
1048
|
+
),
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
class QuantitativeAxisStyle(BaseAxisStyle):
|
|
1053
|
+
"""Scale-type overlay for quantitative axes. Theme slot: axis_quantitative.
|
|
1054
|
+
|
|
1055
|
+
Thin today (no extra fields beyond BaseAxisStyle). Subclassed to give
|
|
1056
|
+
quantitative-axis patches a distinct type so AxisOverrides can be precisely
|
|
1057
|
+
typed per slot.
|
|
1058
|
+
"""
|
|
1059
|
+
|
|
1060
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
class BandAxisStyle(BaseAxisStyle):
|
|
1064
|
+
"""Scale-type overlay for band (categorical) axes. Theme slot: axis_band.
|
|
1065
|
+
|
|
1066
|
+
Adds band_position — structurally absent from all other variants.
|
|
1067
|
+
"""
|
|
1068
|
+
|
|
1069
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
1070
|
+
|
|
1071
|
+
band_position: Annotated[float | None, SkipInheritSlots()] = Field(
|
|
1072
|
+
default=None,
|
|
1073
|
+
description="Band position within the step (0–1); None uses Vega-Lite's default.",
|
|
1074
|
+
)
|