dataface 0.4.1.dev32__py3-none-any.whl → 0.4.1.dev53__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dataface/agent_api/diagnostics.py +25 -7
- dataface/agent_api/docs/yaml-reference.md +4 -5
- dataface/core/compile/chart_update.py +23 -19
- dataface/core/compile/migrations/versions/v0_4_0.py +5 -5
- dataface/core/compile/models/config.py +17 -0
- dataface/core/compile/models/style/resolved/_base.py +0 -1
- dataface/core/compile/models/style/resolved/_cascade.py +0 -1
- dataface/core/compile/models/style/theme/__init__.py +0 -2
- dataface/core/compile/models/style/theme/axis.py +0 -11
- dataface/core/compile/models/style/theme/board.py +21 -67
- dataface/core/compile/resolve/__init__.py +2 -4
- dataface/core/compile/resolve/_dispatch.py +2 -95
- dataface/core/compile/resolve/_domain.py +1 -68
- dataface/core/compile/resolve/area.py +5 -19
- dataface/core/compile/resolve/bar.py +0 -15
- dataface/core/compile/resolve/line.py +5 -20
- dataface/core/compile/resolve/pie.py +52 -53
- dataface/core/compile/resolve/pie_attachment.py +56 -1
- dataface/core/compile/resolve/scatter.py +18 -31
- dataface/core/defaults/default_config.yml +39 -0
- dataface/core/defaults/themes/_base.yaml +9 -10
- dataface/core/diagnostics/__init__.py +0 -2
- dataface/core/diagnostics/codes_execute.py +0 -23
- dataface/core/execute/chart_resolution.py +5 -199
- dataface/core/font_measure.py +20 -0
- dataface/core/render/column_packer.py +236 -0
- dataface/core/render/faces.py +16 -189
- dataface/core/render/prose.py +295 -0
- dataface/core/render/sizing.py +35 -164
- dataface/core/render/svg_utils.py +6 -0
- dataface/core/render/templates/controls/_styles.css +187 -8
- dataface/core/render/templates/scripts/variables.js +345 -50
- dataface/core/render/variables_strip.py +6 -0
- dataface/data/schemas/board-resolved/board-resolved.schema.json +4 -54
- {dataface-0.4.1.dev32.dist-info → dataface-0.4.1.dev53.dist-info}/METADATA +1 -1
- {dataface-0.4.1.dev32.dist-info → dataface-0.4.1.dev53.dist-info}/RECORD +42 -40
- mdsvg/fonts.py +28 -2
- mdsvg/renderer.py +132 -2
- mdsvg/style.py +6 -0
- {dataface-0.4.1.dev32.dist-info → dataface-0.4.1.dev53.dist-info}/WHEEL +0 -0
- {dataface-0.4.1.dev32.dist-info → dataface-0.4.1.dev53.dist-info}/entry_points.txt +0 -0
- {dataface-0.4.1.dev32.dist-info → dataface-0.4.1.dev53.dist-info}/licenses/LICENSE +0 -0
|
@@ -36,9 +36,10 @@ __all__ = [
|
|
|
36
36
|
"DiagnosticCodeDetail",
|
|
37
37
|
"DiagnosticCodesListResult",
|
|
38
38
|
"DiagnosticCodeDetailResult",
|
|
39
|
+
"DiagnosticDisplay",
|
|
39
40
|
"list_diagnostic_codes",
|
|
40
41
|
"get_diagnostic_code",
|
|
41
|
-
"
|
|
42
|
+
"display_map",
|
|
42
43
|
]
|
|
43
44
|
|
|
44
45
|
# ----- Args types ------------------------------------------------------------
|
|
@@ -77,6 +78,20 @@ class DiagnosticCodeSummary(BaseModel):
|
|
|
77
78
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
78
79
|
|
|
79
80
|
|
|
81
|
+
class DiagnosticDisplay(BaseModel):
|
|
82
|
+
"""Registry-derived presentation data for one code, shipped per page.
|
|
83
|
+
|
|
84
|
+
Both fields are derived DiagnosticCode properties rather than serialized
|
|
85
|
+
Diagnostic fields, so a display surface resolves them from the code it
|
|
86
|
+
already has instead of receiving them on every payload.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
title: str
|
|
90
|
+
doc_url: str
|
|
91
|
+
|
|
92
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
93
|
+
|
|
94
|
+
|
|
80
95
|
class DiagnosticCodeDetail(BaseModel):
|
|
81
96
|
"""Full details for a single diagnostic code."""
|
|
82
97
|
|
|
@@ -207,11 +222,14 @@ def get_diagnostic_code(code: str) -> DiagnosticCodeDetailResult:
|
|
|
207
222
|
)
|
|
208
223
|
|
|
209
224
|
|
|
210
|
-
def
|
|
211
|
-
"""The registry's code ->
|
|
225
|
+
def display_map() -> dict[str, DiagnosticDisplay]:
|
|
226
|
+
"""The registry's code -> display map, built once for embedding alongside a page.
|
|
212
227
|
|
|
213
|
-
doc_url
|
|
214
|
-
|
|
215
|
-
re-deriving or
|
|
228
|
+
`title` and `doc_url` are derived DiagnosticCode properties, never
|
|
229
|
+
authored/serialized Diagnostic fields — Cloud and Playground each ship this
|
|
230
|
+
map once rather than re-deriving a URL or restating a title per diagnostic.
|
|
216
231
|
"""
|
|
217
|
-
return {
|
|
232
|
+
return {
|
|
233
|
+
dc.code: DiagnosticDisplay(title=dc.title, doc_url=dc.doc_url)
|
|
234
|
+
for dc in REGISTRY.all()
|
|
235
|
+
}
|
|
@@ -1894,14 +1894,14 @@ Authored overlay for BlockMarginStyle. Top and bottom margin for a prose block r
|
|
|
1894
1894
|
|
|
1895
1895
|
<a id="textcolumnstyle"></a>
|
|
1896
1896
|
## TextColumnStyle
|
|
1897
|
-
Authored overlay for TextColumnStyle.
|
|
1897
|
+
Authored overlay for TextColumnStyle. Author overrides for the column layout of face body text.
|
|
1898
1898
|
|
|
1899
1899
|
| Field | Type | Optional | Description |
|
|
1900
1900
|
|-------|------|:--------:|-------------|
|
|
1901
|
-
| `number` | int | ✓ |
|
|
1902
|
-
| `gap` | float | ✓ | Gap between columns in pixels. None =
|
|
1901
|
+
| `number` | int | ✓ | Ceiling on the column count. The renderer may choose fewer when there is not enough text to fill them. None = no ceiling. |
|
|
1902
|
+
| `gap` | float | ✓ | Gap between columns in pixels. None = 1.5 line boxes, so the gutter scales with the type it separates. |
|
|
1903
1903
|
| `rule` | str | ✓ | CSS column-rule shorthand, e.g. '1px solid #e5e7eb'. None = no rule. |
|
|
1904
|
-
| `max_chars` | int | ✓ |
|
|
1904
|
+
| `max_chars` | int | ✓ | Column width as a character count, overriding the shipped measure. The width is used exactly and the column count follows from it. |
|
|
1905
1905
|
|
|
1906
1906
|
<a id="textcodestyle"></a>
|
|
1907
1907
|
## TextCodeStyle
|
|
@@ -2335,7 +2335,6 @@ Authored overlay for ScaleContinuousStyle. Continuous-scale-only config: type, d
|
|
|
2335
2335
|
| `zero` | bool \| enum: "auto" | ✓ | Force scale zero-baseline: True/False pins it; "auto" runs the Dataface smart-zero heuristic; None passes through to Vega-Lite. |
|
|
2336
2336
|
| `type` | enum: "linear", "log", "pow", "sqrt", "symlog", "temporal" | ✓ | Scale type override. 'linear'/'log'/'pow'/'sqrt'/'symlog' pass straight through to Vega-Lite's quantitative scale.type. 'temporal' is a Dataface escape hatch for a cartesian x-axis: it forces a continuous temporal scale instead of the auto-inferred ordinal/nominal bucketed type, which is required before an authored ``domain`` of ISO dates can extend the visible range past the data extent. None lets Dataface/Vega-Lite infer from the field type. |
|
|
2337
2337
|
| `domain` | list[Any] | ✓ | Explicit [low, high] scale domain; None lets Vega-Lite auto-determine from data. Must have exactly 2 elements — Dataface only wires a continuous range override through to Vega-Lite, not an explicit category enumeration. When ``type: temporal`` is set, both elements must be ISO-8601 date/datetime strings. |
|
|
2338
|
-
| `domain_strategy` | enum: "stable", "auto" | ✓ | How the numeric domain responds to filter-variable changes when no explicit `domain` is authored. 'stable' (the default when None) pins the domain to the chart query's full unfiltered extent — the same value lands at the same pixel across variable changes. 'auto' rescales the domain to the current filtered subset on every variable change. An explicit `domain` always wins over either strategy. |
|
|
2339
2338
|
| `log` | [ScaleLogStyle](#scalelogstyle) | ✓ | Log-scale param (base); only meaningful with type: log. |
|
|
2340
2339
|
| `pow` | [ScalePowStyle](#scalepowstyle) | ✓ | Power-scale param (exponent); only meaningful with type: pow. |
|
|
2341
2340
|
| `symlog` | [ScaleSymlogStyle](#scalesymlogstyle) | ✓ | Symlog-scale param (constant); only meaningful with type: symlog. |
|
|
@@ -36,13 +36,10 @@ Usage:
|
|
|
36
36
|
)
|
|
37
37
|
"""
|
|
38
38
|
|
|
39
|
-
import logging
|
|
40
39
|
from typing import Any
|
|
41
40
|
|
|
42
41
|
import yaml
|
|
43
42
|
|
|
44
|
-
logger = logging.getLogger(__name__)
|
|
45
|
-
|
|
46
43
|
|
|
47
44
|
def update_chart_in_yaml(
|
|
48
45
|
yaml_content: str,
|
|
@@ -147,6 +144,25 @@ def update_chart_in_yaml(
|
|
|
147
144
|
if field in edits and edits[field]:
|
|
148
145
|
chart_def[field] = edits[field]
|
|
149
146
|
|
|
147
|
+
# Validate rewritability before any mutation.
|
|
148
|
+
# A rows/cols source_path must be exactly two segments (layout_key.N) where N
|
|
149
|
+
# is a digit — that is the only shape _update_layout_reference can rewrite.
|
|
150
|
+
# Deeper paths (rows.0.cols.1) name a chart inside a nested face; the nested
|
|
151
|
+
# face's own file must name the chart instead.
|
|
152
|
+
if source_path and source_path.split(".")[0] in ("rows", "cols"):
|
|
153
|
+
_segs = source_path.split(".")
|
|
154
|
+
if len(_segs) != 2:
|
|
155
|
+
raise ValueError(
|
|
156
|
+
f"Cannot promote inline chart at '{source_path}': only top-level "
|
|
157
|
+
"layout positions (rows.N / cols.N) are supported; name the chart "
|
|
158
|
+
"in the nested face's own file instead"
|
|
159
|
+
)
|
|
160
|
+
if not _segs[1].isdigit():
|
|
161
|
+
raise ValueError(
|
|
162
|
+
f"Cannot promote inline chart at '{source_path}': "
|
|
163
|
+
f"'{_segs[1]}' is not a valid integer index"
|
|
164
|
+
)
|
|
165
|
+
|
|
150
166
|
# Update the charts section
|
|
151
167
|
# If chart already exists, merge with existing values
|
|
152
168
|
if chart_id in data["charts"]:
|
|
@@ -171,31 +187,19 @@ def update_chart_in_yaml(
|
|
|
171
187
|
def _update_layout_reference(
|
|
172
188
|
data: dict[str, Any], source_path: str, chart_id: str
|
|
173
189
|
) -> None:
|
|
174
|
-
"""
|
|
190
|
+
"""Replace an inline chart at a top-level layout position with a named reference.
|
|
175
191
|
|
|
176
192
|
Args:
|
|
177
193
|
data: Parsed YAML data (modified in place)
|
|
178
|
-
source_path:
|
|
194
|
+
source_path: Validated path of the form "rows.N" or "cols.N".
|
|
195
|
+
Caller guarantees exactly two segments and a digit index.
|
|
179
196
|
chart_id: ID of the chart to reference
|
|
180
197
|
"""
|
|
181
|
-
|
|
182
|
-
# `data[layout_key]`, and a deeper path (`rows.0.cols.1`) names an entry in a
|
|
183
|
-
# nested face's own list, which this function never walks into.
|
|
184
|
-
segments = source_path.split(".")
|
|
185
|
-
if len(segments) != 2 or segments[0] not in ("rows", "cols"):
|
|
186
|
-
logger.debug(f"Unrecognized source_path format: {source_path}")
|
|
187
|
-
return
|
|
188
|
-
layout_key, raw_index = segments
|
|
189
|
-
if not raw_index.isdigit():
|
|
190
|
-
logger.warning(f"Failed to parse index from source_path: {source_path}")
|
|
191
|
-
return
|
|
198
|
+
layout_key, raw_index = source_path.split(".")
|
|
192
199
|
index = int(raw_index)
|
|
193
|
-
|
|
194
|
-
# Update the layout
|
|
195
200
|
if (
|
|
196
201
|
layout_key in data
|
|
197
202
|
and isinstance(data[layout_key], list)
|
|
198
203
|
and 0 <= index < len(data[layout_key])
|
|
199
204
|
):
|
|
200
|
-
# Replace inline chart with named reference
|
|
201
205
|
data[layout_key][index] = chart_id
|
|
@@ -8,10 +8,11 @@ leaf move creates its destination container on demand, and the later
|
|
|
8
8
|
container move must find that destination still empty (or absent) to avoid
|
|
9
9
|
a spurious MigrationConflictError against an unrelated field that also sets
|
|
10
10
|
the old container. See dataface/AGENTS.md's "Two validation boundaries" —
|
|
11
|
-
these are structural moves; categorical_orient (deleted),
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
raise UnsupportedSchemaError for
|
|
11
|
+
these are structural moves; categorical_orient (deleted), the stable-axis
|
|
12
|
+
domain-strategy key (deleted), ticks.interval (reinterpreted vocabulary, not
|
|
13
|
+
renamed), and the band/point/quantize scale groups (deleted) are semantic
|
|
14
|
+
changes with no lossless move and are left to raise UnsupportedSchemaError for
|
|
15
|
+
manual migration.
|
|
15
16
|
"""
|
|
16
17
|
|
|
17
18
|
from __future__ import annotations
|
|
@@ -29,7 +30,6 @@ RENAMES: tuple[tuple[YamlKeyPath, YamlKeyPath], ...] = (
|
|
|
29
30
|
(("scale", "continuous", "domain"), ("scale", "domain")),
|
|
30
31
|
(("scale", "continuous", "zero"), ("scale", "zero")),
|
|
31
32
|
(("scale", "continuous", "type"), ("scale", "type")),
|
|
32
|
-
(("scale", "continuous", "domain_strategy"), ("scale", "domain_strategy")),
|
|
33
33
|
(("scale", "continuous", "log", "base"), ("scale", "base")),
|
|
34
34
|
(("scale", "continuous", "pow", "exponent"), ("scale", "exponent")),
|
|
35
35
|
(("scale", "continuous", "symlog", "constant"), ("scale", "constant")),
|
|
@@ -162,6 +162,22 @@ class ChartRenderingConfig(ConfigNode):
|
|
|
162
162
|
footer_rule_gap_px: int
|
|
163
163
|
footer_timestamp_gap_px: int
|
|
164
164
|
|
|
165
|
+
class ProseConfig(ConfigNode):
|
|
166
|
+
target_chars: int
|
|
167
|
+
single_column_chars: int
|
|
168
|
+
min_chars: int
|
|
169
|
+
max_chars: int
|
|
170
|
+
min_lines_per_column: int
|
|
171
|
+
max_columns: int
|
|
172
|
+
gutter_line_boxes: float
|
|
173
|
+
orphan_penalty: float
|
|
174
|
+
widow_penalty: float
|
|
175
|
+
interior_break_penalty: float
|
|
176
|
+
block_break_penalty: float
|
|
177
|
+
balance_weight: float
|
|
178
|
+
balance_scale: float
|
|
179
|
+
balance_undershoot: float
|
|
180
|
+
|
|
165
181
|
class DataTableConfig(ConfigNode):
|
|
166
182
|
divider_gap: float
|
|
167
183
|
chart_data_table_max_x_ticks: int
|
|
@@ -174,6 +190,7 @@ class ChartRenderingConfig(ConfigNode):
|
|
|
174
190
|
bar: BarConfig
|
|
175
191
|
type_inference: TypeInferenceConfig
|
|
176
192
|
board: BoardConfig
|
|
193
|
+
prose: ProseConfig
|
|
177
194
|
data_table: DataTableConfig
|
|
178
195
|
stroke: StrokeConfig
|
|
179
196
|
axis: AxisConfig
|
|
@@ -201,7 +201,6 @@ class ResolvedScaleContinuousStyle(BaseModel):
|
|
|
201
201
|
zero: bool | Literal["auto"] | None = None
|
|
202
202
|
type: Literal["linear", "log", "pow", "sqrt", "symlog", "temporal"] | None = None
|
|
203
203
|
domain: list[Any] | None = None
|
|
204
|
-
domain_strategy: Literal["stable", "auto"] | None = None
|
|
205
204
|
log: ResolvedScaleLogStyle | None = None
|
|
206
205
|
pow: ResolvedScalePowStyle | None = None
|
|
207
206
|
symlog: ResolvedScaleSymlogStyle | None = None
|
|
@@ -572,7 +572,6 @@ def build_resolved_scale(
|
|
|
572
572
|
zero=scale.continuous.zero,
|
|
573
573
|
type=scale.continuous.type,
|
|
574
574
|
domain=scale.continuous.domain,
|
|
575
|
-
domain_strategy=scale.continuous.domain_strategy,
|
|
576
575
|
log=(
|
|
577
576
|
ResolvedScaleLogStyle(base=scale.continuous.log.base)
|
|
578
577
|
if scale.continuous.log is not None
|
|
@@ -104,7 +104,6 @@ from dataface.core.compile.models.style.theme.board import (
|
|
|
104
104
|
TitleWidthOffsetsStyle,
|
|
105
105
|
_normalize_overflow_value,
|
|
106
106
|
coerce_gap,
|
|
107
|
-
compute_text_column_count,
|
|
108
107
|
font_weight_as_css,
|
|
109
108
|
)
|
|
110
109
|
from dataface.core.compile.models.style.theme.callout import (
|
|
@@ -414,6 +413,5 @@ __all__ = [
|
|
|
414
413
|
"_RadialChartStyle",
|
|
415
414
|
"_normalize_overflow_value",
|
|
416
415
|
"coerce_gap",
|
|
417
|
-
"compute_text_column_count",
|
|
418
416
|
"font_weight_as_css",
|
|
419
417
|
]
|
|
@@ -585,17 +585,6 @@ class ScaleContinuousStyle(ScaleDomainValidationMixin):
|
|
|
585
585
|
"date/datetime strings."
|
|
586
586
|
),
|
|
587
587
|
)
|
|
588
|
-
domain_strategy: Literal["stable", "auto"] | None = Field(
|
|
589
|
-
default=None,
|
|
590
|
-
description=(
|
|
591
|
-
"How the numeric domain responds to filter-variable changes when no "
|
|
592
|
-
"explicit `domain` is authored. 'stable' (the default when None) pins "
|
|
593
|
-
"the domain to the chart query's full unfiltered extent — the same "
|
|
594
|
-
"value lands at the same pixel across variable changes. 'auto' "
|
|
595
|
-
"rescales the domain to the current filtered subset on every variable "
|
|
596
|
-
"change. An explicit `domain` always wins over either strategy."
|
|
597
|
-
),
|
|
598
|
-
)
|
|
599
588
|
log: ScaleLogStyle | None = Field(
|
|
600
589
|
default=None,
|
|
601
590
|
description="Log-scale param (base); only meaningful with type: log.",
|
|
@@ -68,26 +68,35 @@ _COLUMN_RULE_HEX_RE = re.compile(
|
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
class TextColumnStyle(BaseModel):
|
|
71
|
-
"""
|
|
71
|
+
"""Author overrides for the column layout of face body text.
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
and column count auto-derives as ``ceil(available / col_width_px)``.
|
|
73
|
+
Body text is measured and flowed whether or not any of these are set: the
|
|
74
|
+
renderer picks a column count whose measure reads well, and how much text
|
|
75
|
+
there is decides how many of those columns are worth using. These narrow
|
|
76
|
+
that choice rather than switching it on.
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
- ``number``: ceiling on the count. The renderer may still choose fewer,
|
|
79
|
+
when the text is too short to fill them.
|
|
80
|
+
- ``max_chars``: the measure itself, overriding the shipped default. The
|
|
81
|
+
width is used exactly and the count follows from it.
|
|
81
82
|
"""
|
|
82
83
|
|
|
83
84
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
84
85
|
|
|
85
86
|
number: int | None = Field(
|
|
86
|
-
default=None,
|
|
87
|
+
default=None,
|
|
88
|
+
ge=1,
|
|
89
|
+
description=(
|
|
90
|
+
"Ceiling on the column count. The renderer may choose fewer when there "
|
|
91
|
+
"is not enough text to fill them. None = no ceiling."
|
|
92
|
+
),
|
|
87
93
|
)
|
|
88
94
|
gap: float | None = Field(
|
|
89
95
|
default=None,
|
|
90
|
-
description=
|
|
96
|
+
description=(
|
|
97
|
+
"Gap between columns in pixels. None = 1.5 line boxes, so the gutter "
|
|
98
|
+
"scales with the type it separates."
|
|
99
|
+
),
|
|
91
100
|
)
|
|
92
101
|
rule: str | None = Field(
|
|
93
102
|
default=None,
|
|
@@ -97,9 +106,8 @@ class TextColumnStyle(BaseModel):
|
|
|
97
106
|
default=None,
|
|
98
107
|
gt=0,
|
|
99
108
|
description=(
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"auto-derives as ceil(available / col_width_px)."
|
|
109
|
+
"Column width as a character count, overriding the shipped measure. "
|
|
110
|
+
"The width is used exactly and the column count follows from it."
|
|
103
111
|
),
|
|
104
112
|
)
|
|
105
113
|
|
|
@@ -134,60 +142,6 @@ class TextColumnStyle(BaseModel):
|
|
|
134
142
|
)
|
|
135
143
|
return v
|
|
136
144
|
|
|
137
|
-
@model_validator(mode="after")
|
|
138
|
-
def _validate_column_settings(self) -> TextColumnStyle:
|
|
139
|
-
multi = (
|
|
140
|
-
self.number is not None and self.number > 1
|
|
141
|
-
) or self.max_chars is not None
|
|
142
|
-
if self.gap is not None and not multi:
|
|
143
|
-
raise ValueError(
|
|
144
|
-
"column.gap requires column.number > 1 or column.max_chars"
|
|
145
|
-
)
|
|
146
|
-
if self.rule is not None and not multi:
|
|
147
|
-
raise ValueError(
|
|
148
|
-
"column.rule requires column.number > 1 or column.max_chars"
|
|
149
|
-
)
|
|
150
|
-
return self
|
|
151
|
-
|
|
152
|
-
@property
|
|
153
|
-
def has_overrides(self) -> bool:
|
|
154
|
-
"""True when multi-column layout is configured."""
|
|
155
|
-
return (
|
|
156
|
-
self.number is not None and self.number > 1
|
|
157
|
-
) or self.max_chars is not None
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def compute_text_column_count(
|
|
161
|
-
col: TextColumnStyle,
|
|
162
|
-
available_width: float,
|
|
163
|
-
col_width_px: float | None = None,
|
|
164
|
-
) -> int:
|
|
165
|
-
"""Auto-derive column count, with number as ceiling.
|
|
166
|
-
|
|
167
|
-
When ``col_width_px`` is provided (derived from max_chars × avg_char_width):
|
|
168
|
-
count = max(1, ceil(available / col_width_px)).
|
|
169
|
-
When only number is set: count = number.
|
|
170
|
-
When both are set: count = min(number, auto-derived).
|
|
171
|
-
When neither is set: single column.
|
|
172
|
-
|
|
173
|
-
``col_width_px`` must be provided when col.max_chars is set; it is derived by
|
|
174
|
-
the caller via fonttools OS/2.xAvgCharWidth × max_chars × (font_size / upm).
|
|
175
|
-
"""
|
|
176
|
-
import math
|
|
177
|
-
|
|
178
|
-
if col.max_chars is not None and col_width_px is None:
|
|
179
|
-
raise ValueError(
|
|
180
|
-
"col_width_px must be provided when col.max_chars is set — the "
|
|
181
|
-
"caller must derive it from max_chars before calling "
|
|
182
|
-
"compute_text_column_count."
|
|
183
|
-
)
|
|
184
|
-
if col_width_px is not None:
|
|
185
|
-
auto = max(1, math.ceil(available_width / col_width_px))
|
|
186
|
-
return min(col.number, auto) if col.number is not None else auto
|
|
187
|
-
if col.number is not None:
|
|
188
|
-
return col.number
|
|
189
|
-
return 1
|
|
190
|
-
|
|
191
145
|
|
|
192
146
|
def coerce_gap(v: Any) -> float | None:
|
|
193
147
|
"""Coerce gap value from int, float, or CSS px string."""
|
|
@@ -25,8 +25,8 @@ one-way: shared modules never import a family module, and no family module impor
|
|
|
25
25
|
another family module — only `_dispatch` imports family modules.
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
|
-
from ._dispatch import
|
|
29
|
-
from ._domain import
|
|
28
|
+
from ._dispatch import resolve
|
|
29
|
+
from ._domain import resolve_y_zero
|
|
30
30
|
from ._kwargs import (
|
|
31
31
|
AutomaticLinkCandidate,
|
|
32
32
|
default_chart_width,
|
|
@@ -42,11 +42,9 @@ from ._table import infer_pivot_measure_names
|
|
|
42
42
|
|
|
43
43
|
__all__ = [
|
|
44
44
|
"AutomaticLinkCandidate",
|
|
45
|
-
"StableYDomain",
|
|
46
45
|
"auto_link_excludes_x",
|
|
47
46
|
"default_chart_width",
|
|
48
47
|
"infer_pivot_measure_names",
|
|
49
|
-
"needs_stable_y_domain",
|
|
50
48
|
"preferred_chart_width",
|
|
51
49
|
"resolve",
|
|
52
50
|
"resolve_chart_display_title",
|
|
@@ -5,13 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from collections.abc import Mapping, Sequence
|
|
6
6
|
from types import EllipsisType
|
|
7
7
|
|
|
8
|
-
from dataface.core.compile.models.chart.normalized import
|
|
9
|
-
AreaChart,
|
|
10
|
-
BarChart,
|
|
11
|
-
Chart,
|
|
12
|
-
LineChart,
|
|
13
|
-
ScatterChart,
|
|
14
|
-
)
|
|
8
|
+
from dataface.core.compile.models.chart.normalized import Chart
|
|
15
9
|
from dataface.core.compile.models.chart.resolved import (
|
|
16
10
|
ResolvedChart,
|
|
17
11
|
)
|
|
@@ -19,35 +13,21 @@ from dataface.core.compile.models.primitives import ResolvedFontStyle
|
|
|
19
13
|
from dataface.core.compile.models.style.resolved import (
|
|
20
14
|
ResolvedStyle,
|
|
21
15
|
)
|
|
22
|
-
from dataface.core.compile.resolve._axes import (
|
|
23
|
-
_bake_cartesian_axes,
|
|
24
|
-
_extract_axis_overrides,
|
|
25
|
-
)
|
|
26
|
-
from dataface.core.compile.resolve._channels import _classify_to_channel_type
|
|
27
16
|
from dataface.core.compile.resolve._chart_rows import (
|
|
28
17
|
ChartRows,
|
|
29
18
|
LayerDatasets,
|
|
30
19
|
)
|
|
31
|
-
from dataface.core.compile.resolve._domain import (
|
|
32
|
-
StableYDomain,
|
|
33
|
-
_first_non_numeric_y,
|
|
34
|
-
_stable_domain_enabled,
|
|
35
|
-
)
|
|
36
20
|
from dataface.core.compile.resolve._kwargs import (
|
|
37
21
|
_EMPTY_CHART_TEXT_VARIABLES,
|
|
38
22
|
AutomaticLinkCandidate,
|
|
39
23
|
ChartTextVariables,
|
|
40
24
|
preferred_chart_width,
|
|
41
25
|
)
|
|
42
|
-
from dataface.core.compile.resolve._palette import _with_color_tokens
|
|
43
26
|
from dataface.core.compile.resolve._table import (
|
|
44
27
|
_EMPTY_TABLE_COLUMN_LINKS,
|
|
45
28
|
_resolve_table,
|
|
46
29
|
)
|
|
47
|
-
from dataface.core.compile.resolve.area import
|
|
48
|
-
_resolve_area,
|
|
49
|
-
_validate_area_encoding,
|
|
50
|
-
)
|
|
30
|
+
from dataface.core.compile.resolve.area import _resolve_area
|
|
51
31
|
from dataface.core.compile.resolve.bar import (
|
|
52
32
|
_resolve_bar,
|
|
53
33
|
_resolve_histogram,
|
|
@@ -65,85 +45,18 @@ from dataface.core.compile.resolve.simple import (
|
|
|
65
45
|
_resolve_kpi,
|
|
66
46
|
_resolve_spark_bar,
|
|
67
47
|
)
|
|
68
|
-
from dataface.core.compile.style_cascade import (
|
|
69
|
-
build_resolved_style,
|
|
70
|
-
)
|
|
71
48
|
|
|
72
49
|
__all__ = [
|
|
73
|
-
"needs_stable_y_domain",
|
|
74
50
|
"resolve",
|
|
75
51
|
]
|
|
76
52
|
|
|
77
53
|
|
|
78
|
-
def needs_stable_y_domain(
|
|
79
|
-
normalized: Chart,
|
|
80
|
-
data: ChartRows,
|
|
81
|
-
board_style: ResolvedStyle,
|
|
82
|
-
) -> bool:
|
|
83
|
-
"""Whether runtime orchestration should gather a stable y extent.
|
|
84
|
-
|
|
85
|
-
This is a compile-owned pre-resolution decision: render may execute the
|
|
86
|
-
requested sibling queries, but it does not inspect style cascade results.
|
|
87
|
-
"""
|
|
88
|
-
if not isinstance(normalized, (BarChart, LineChart, AreaChart, ScatterChart)):
|
|
89
|
-
return False
|
|
90
|
-
if normalized.type == "histogram" or not isinstance(normalized.y, str):
|
|
91
|
-
return False
|
|
92
|
-
|
|
93
|
-
board_rcs = board_style.charts
|
|
94
|
-
primary = _with_color_tokens(normalized.style, board_style)
|
|
95
|
-
charts_style = (
|
|
96
|
-
build_resolved_style(board_style, normalized)
|
|
97
|
-
if isinstance(normalized, (BarChart, LineChart, AreaChart))
|
|
98
|
-
else board_rcs
|
|
99
|
-
)
|
|
100
|
-
x_channel_type = _classify_to_channel_type(normalized.x, data, is_dimension=True)
|
|
101
|
-
if isinstance(normalized, ScatterChart):
|
|
102
|
-
y_channel_type = _classify_to_channel_type(
|
|
103
|
-
normalized.y, data, is_dimension=False
|
|
104
|
-
)
|
|
105
|
-
if y_channel_type != "quantitative":
|
|
106
|
-
return False
|
|
107
|
-
elif isinstance(normalized, AreaChart):
|
|
108
|
-
resolved_stack = (
|
|
109
|
-
normalized.stack
|
|
110
|
-
if normalized.stack is not None
|
|
111
|
-
else charts_style.area.stack
|
|
112
|
-
)
|
|
113
|
-
_validate_area_encoding(normalized, data, x_channel_type, resolved_stack)
|
|
114
|
-
y_channel_type = "quantitative"
|
|
115
|
-
else:
|
|
116
|
-
if _first_non_numeric_y(normalized.y, data) is not None:
|
|
117
|
-
return False
|
|
118
|
-
y_channel_type = "quantitative"
|
|
119
|
-
_, ay, _, _ = _bake_cartesian_axes(
|
|
120
|
-
charts_style,
|
|
121
|
-
normalized,
|
|
122
|
-
normalized.type,
|
|
123
|
-
x_channel_type,
|
|
124
|
-
y_channel_type,
|
|
125
|
-
_extract_axis_overrides(primary, normalized.x_label, normalized.y_label),
|
|
126
|
-
multiples=normalized.multiples,
|
|
127
|
-
y=normalized.y,
|
|
128
|
-
)
|
|
129
|
-
if not _stable_domain_enabled(ay):
|
|
130
|
-
return False
|
|
131
|
-
if isinstance(normalized, BarChart):
|
|
132
|
-
stack = (
|
|
133
|
-
normalized.stack if normalized.stack is not None else charts_style.bar.stack
|
|
134
|
-
)
|
|
135
|
-
if stack == "normalize":
|
|
136
|
-
return False
|
|
137
|
-
return True
|
|
138
|
-
|
|
139
|
-
|
|
140
54
|
def resolve(
|
|
141
55
|
normalized: Chart,
|
|
142
56
|
data: ChartRows,
|
|
143
57
|
board_style: ResolvedStyle,
|
|
144
58
|
width: float | None = None,
|
|
145
59
|
datasets: LayerDatasets = ...,
|
|
146
|
-
stable_y_domain: StableYDomain | EllipsisType = ...,
|
|
147
60
|
automatic_link_candidate: AutomaticLinkCandidate = None,
|
|
148
61
|
table_column_links: Mapping[str, str] = _EMPTY_TABLE_COLUMN_LINKS,
|
|
149
62
|
table_column_rows: Sequence[Mapping[str, str]] = (),
|
|
@@ -164,8 +77,6 @@ def resolve(
|
|
|
164
77
|
needs a concrete width; other families retain the board-level default.
|
|
165
78
|
datasets: Query-name keyed rows for own-query overlay layers. Callers without
|
|
166
79
|
an executor may omit it; same-query layers still resolve from ``data``.
|
|
167
|
-
stable_y_domain: Query-derived extent gathered by runtime
|
|
168
|
-
orchestration after ``needs_stable_y_domain()`` requests it.
|
|
169
80
|
automatic_link_candidate: Executor-derived root-link candidate. Authored
|
|
170
81
|
links retain precedence during construction.
|
|
171
82
|
table_column_links: Executor-derived FK-link candidates by column name.
|
|
@@ -198,7 +109,6 @@ def resolve(
|
|
|
198
109
|
board_style,
|
|
199
110
|
effective_width,
|
|
200
111
|
datasets,
|
|
201
|
-
stable_y_domain,
|
|
202
112
|
automatic_link_candidate,
|
|
203
113
|
variables,
|
|
204
114
|
)
|
|
@@ -218,7 +128,6 @@ def resolve(
|
|
|
218
128
|
board_style,
|
|
219
129
|
effective_width,
|
|
220
130
|
datasets,
|
|
221
|
-
stable_y_domain,
|
|
222
131
|
automatic_link_candidate,
|
|
223
132
|
variables,
|
|
224
133
|
)
|
|
@@ -229,7 +138,6 @@ def resolve(
|
|
|
229
138
|
board_style,
|
|
230
139
|
effective_width,
|
|
231
140
|
datasets,
|
|
232
|
-
stable_y_domain,
|
|
233
141
|
automatic_link_candidate,
|
|
234
142
|
variables,
|
|
235
143
|
)
|
|
@@ -240,7 +148,6 @@ def resolve(
|
|
|
240
148
|
board_style,
|
|
241
149
|
effective_width,
|
|
242
150
|
datasets,
|
|
243
|
-
stable_y_domain,
|
|
244
151
|
automatic_link_candidate,
|
|
245
152
|
variables,
|
|
246
153
|
)
|