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.
Files changed (42) hide show
  1. dataface/agent_api/diagnostics.py +25 -7
  2. dataface/agent_api/docs/yaml-reference.md +4 -5
  3. dataface/core/compile/chart_update.py +23 -19
  4. dataface/core/compile/migrations/versions/v0_4_0.py +5 -5
  5. dataface/core/compile/models/config.py +17 -0
  6. dataface/core/compile/models/style/resolved/_base.py +0 -1
  7. dataface/core/compile/models/style/resolved/_cascade.py +0 -1
  8. dataface/core/compile/models/style/theme/__init__.py +0 -2
  9. dataface/core/compile/models/style/theme/axis.py +0 -11
  10. dataface/core/compile/models/style/theme/board.py +21 -67
  11. dataface/core/compile/resolve/__init__.py +2 -4
  12. dataface/core/compile/resolve/_dispatch.py +2 -95
  13. dataface/core/compile/resolve/_domain.py +1 -68
  14. dataface/core/compile/resolve/area.py +5 -19
  15. dataface/core/compile/resolve/bar.py +0 -15
  16. dataface/core/compile/resolve/line.py +5 -20
  17. dataface/core/compile/resolve/pie.py +52 -53
  18. dataface/core/compile/resolve/pie_attachment.py +56 -1
  19. dataface/core/compile/resolve/scatter.py +18 -31
  20. dataface/core/defaults/default_config.yml +39 -0
  21. dataface/core/defaults/themes/_base.yaml +9 -10
  22. dataface/core/diagnostics/__init__.py +0 -2
  23. dataface/core/diagnostics/codes_execute.py +0 -23
  24. dataface/core/execute/chart_resolution.py +5 -199
  25. dataface/core/font_measure.py +20 -0
  26. dataface/core/render/column_packer.py +236 -0
  27. dataface/core/render/faces.py +16 -189
  28. dataface/core/render/prose.py +295 -0
  29. dataface/core/render/sizing.py +35 -164
  30. dataface/core/render/svg_utils.py +6 -0
  31. dataface/core/render/templates/controls/_styles.css +187 -8
  32. dataface/core/render/templates/scripts/variables.js +345 -50
  33. dataface/core/render/variables_strip.py +6 -0
  34. dataface/data/schemas/board-resolved/board-resolved.schema.json +4 -54
  35. {dataface-0.4.1.dev32.dist-info → dataface-0.4.1.dev53.dist-info}/METADATA +1 -1
  36. {dataface-0.4.1.dev32.dist-info → dataface-0.4.1.dev53.dist-info}/RECORD +42 -40
  37. mdsvg/fonts.py +28 -2
  38. mdsvg/renderer.py +132 -2
  39. mdsvg/style.py +6 -0
  40. {dataface-0.4.1.dev32.dist-info → dataface-0.4.1.dev53.dist-info}/WHEEL +0 -0
  41. {dataface-0.4.1.dev32.dist-info → dataface-0.4.1.dev53.dist-info}/entry_points.txt +0 -0
  42. {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
- "doc_url_map",
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 doc_url_map() -> dict[str, str]:
211
- """The registry's code -> doc_url map, built once for embedding alongside a page.
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 is a derived DiagnosticCode property, never an authored/serialized
214
- field — Cloud and Playground each ship this map once rather than
215
- re-deriving or hand-formatting a URL per diagnostic.
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 {dc.code: dc.doc_url for dc in REGISTRY.all()}
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. Multi-column layout for face body text (CSS column-count/gap/rule/max_chars).
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 | ✓ | Fixed column count. None = single column. |
1902
- | `gap` | float | ✓ | Gap between columns in pixels. None = use layout gap token. |
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 | ✓ | Target column width as a character count. The pixel width is derived at render time via fonttools OS/2.xAvgCharWidth × max_chars; column count auto-derives as ceil(available / col_width_px). |
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
- """Update a layout position to reference a named chart instead of inline.
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: Path like "rows.0" or "rows.0.cols.1"
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
- # Only a top-level position is rewritable here: this replaces an entry in
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), ticks.interval
12
- (reinterpreted vocabulary, not renamed), and the band/point/quantize scale
13
- groups (deleted) are semantic changes with no lossless move and are left to
14
- raise UnsupportedSchemaError for manual migration.
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
- """Multi-column layout for face body text (CSS column-count/gap/rule/max_chars).
71
+ """Author overrides for the column layout of face body text.
72
72
 
73
- Two orthogonal controls:
74
- - ``number``: fixed column count. None = not set.
75
- - ``max_chars``: target column width as a character count. When set, the
76
- pixel width is derived at render time via fonttools OS/2.xAvgCharWidth,
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
- When both are set, ``number`` acts as a ceiling on the auto-derived count.
80
- When neither is set, text renders as a single column.
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, ge=1, description="Fixed column count. None = single column."
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="Gap between columns in pixels. None = use layout gap token.",
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
- "Target column width as a character count. The pixel width is derived at "
101
- "render time via fonttools OS/2.xAvgCharWidth × max_chars; column count "
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 needs_stable_y_domain, resolve
29
- from ._domain import StableYDomain, resolve_y_zero
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
  )