dataface 0.3.2.dev22__py3-none-any.whl → 0.3.2.dev150__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dataface/DATAFACE_SYNTAX.md +15 -4
- dataface/__init__.py +70 -23
- dataface/agent_api/__init__.py +162 -68
- dataface/agent_api/_paths.py +9 -3
- dataface/agent_api/docs/yaml-reference.md +273 -239
- dataface/agent_api/init.py +1 -1
- dataface/agent_api/project_session.py +2 -1
- dataface/agent_api/serve.py +113 -0
- dataface/agent_api/validate.py +8 -0
- dataface/ai/agent.py +28 -2
- dataface/cli/_project.py +100 -1
- dataface/cli/commands/board_artifact.py +5 -2
- dataface/cli/commands/describe.py +7 -3
- dataface/cli/commands/docs.py +12 -3
- dataface/cli/commands/init.py +7 -14
- dataface/cli/commands/inspect.py +6 -1
- dataface/cli/commands/mcp_init.py +23 -43
- dataface/cli/commands/migrate.py +8 -3
- dataface/cli/commands/query.py +11 -8
- dataface/cli/commands/render.py +26 -9
- dataface/cli/commands/search.py +8 -2
- dataface/cli/commands/serve.py +25 -81
- dataface/cli/commands/skills_init.py +2 -19
- dataface/cli/commands/validate.py +7 -2
- dataface/cli/filesystem_project.py +5 -4
- dataface/cli/main.py +22 -33
- dataface/core/__init__.py +73 -28
- dataface/core/aliases.py +27 -0
- dataface/core/{compile/colors.py → colors.py} +20 -5
- dataface/core/compile/__init__.py +0 -8
- dataface/core/compile/channel.py +0 -13
- dataface/core/compile/compiler.py +161 -96
- dataface/core/compile/data_table.py +7 -2
- dataface/core/compile/enrich.py +18 -44
- dataface/core/compile/face_warnings.py +46 -8
- dataface/core/compile/format.py +47 -1
- dataface/core/compile/inherit_graph.py +3 -3
- dataface/core/compile/inherit_registry.yaml +1 -1
- dataface/core/compile/introspection.py +2 -13
- dataface/core/compile/migrations.py +112 -9
- dataface/core/compile/models/chart/authored/_base.py +1 -1
- dataface/core/compile/models/chart/normalized/__init__.py +2 -0
- dataface/core/compile/models/chart/normalized/_base.py +26 -0
- dataface/core/compile/models/chart/normalized/kpi.py +0 -4
- dataface/core/compile/models/chart/resolved/_base.py +28 -10
- dataface/core/compile/models/chart/resolved/callout.py +2 -3
- dataface/core/compile/models/chart/resolved/kpi.py +7 -3
- dataface/core/compile/models/chart/resolved/table.py +16 -2
- dataface/core/compile/models/config.py +5 -0
- dataface/core/compile/models/face/authored.py +24 -89
- dataface/core/compile/models/markers.py +1 -1
- dataface/core/compile/models/primitives.py +175 -2
- dataface/core/compile/models/query/authored.py +1 -119
- dataface/core/compile/models/refs.py +0 -2
- dataface/core/compile/models/source.py +78 -164
- dataface/core/compile/models/style/authored/__init__.py +32 -8
- dataface/core/compile/models/style/authored/_base.py +126 -69
- dataface/core/compile/models/style/authored/table.py +61 -3
- dataface/core/compile/models/style/resolved/__init__.py +12 -2
- dataface/core/compile/models/style/resolved/_base.py +142 -57
- dataface/core/compile/models/style/resolved/_cartesian.py +6 -11
- dataface/core/compile/models/style/resolved/_cascade.py +141 -54
- dataface/core/compile/models/style/resolved/area.py +1 -1
- dataface/core/compile/models/style/resolved/bar.py +1 -1
- dataface/core/compile/models/style/resolved/line.py +1 -1
- dataface/core/compile/models/style/resolved/scatter.py +3 -2
- dataface/core/compile/models/style/theme/__init__.py +36 -14
- dataface/core/compile/models/style/theme/_chart_base.py +17 -9
- dataface/core/compile/models/style/theme/axis.py +432 -312
- dataface/core/compile/models/style/theme/charts.py +10 -6
- dataface/core/compile/models/style/theme/kpi.py +2 -1
- dataface/core/compile/models/style/theme/page.py +5 -2
- dataface/core/compile/models/style/theme/table.py +2 -2
- dataface/core/compile/models/style/theme/variables.py +0 -3
- dataface/core/compile/models/vega_lite/config.py +1 -1
- dataface/core/compile/normalize_charts.py +10 -154
- dataface/core/compile/normalize_layout.py +20 -284
- dataface/core/compile/normalize_queries.py +24 -513
- dataface/core/compile/normalizer.py +4 -10
- dataface/core/compile/palette.py +19 -7
- dataface/core/compile/resolve/__init__.py +2 -0
- dataface/core/compile/resolve/_axes.py +147 -44
- dataface/core/compile/resolve/_channels.py +1 -1
- dataface/core/compile/resolve/_dispatch.py +1 -1
- dataface/core/compile/resolve/_domain.py +49 -32
- dataface/core/compile/resolve/_kwargs.py +39 -12
- dataface/core/compile/resolve/_marks.py +8 -3
- dataface/core/compile/resolve/_palette.py +7 -3
- dataface/core/compile/resolve/_table.py +124 -33
- dataface/core/compile/resolve/area.py +48 -18
- dataface/core/compile/resolve/bar.py +245 -64
- dataface/core/compile/resolve/geo.py +10 -3
- dataface/core/compile/resolve/heatmap.py +16 -8
- dataface/core/compile/resolve/line.py +35 -12
- dataface/core/compile/resolve/pie.py +4 -1
- dataface/core/compile/resolve/pie_attachment.py +1 -1
- dataface/core/compile/resolve/scatter.py +25 -13
- dataface/core/compile/resolve/simple.py +25 -2
- dataface/core/compile/schema_renderers/prompt.py +1 -1
- dataface/core/compile/schema_renderers/textmate_grammar.py +8 -1
- dataface/core/compile/source_map.py +211 -28
- dataface/core/compile/style_cascade.py +126 -55
- dataface/core/compile/validator.py +121 -44
- dataface/core/compile/yaml_error_formatter.py +108 -3
- dataface/core/connections.py +10 -3
- dataface/core/dashboard.py +27 -19
- dataface/core/defaults/default_config.yml +4 -0
- dataface/core/defaults/themes/_base.yaml +49 -40
- dataface/core/defaults/themes/cream.yaml +4 -4
- dataface/core/defaults/themes/diagnostics-title-baseline.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +1 -1
- dataface/core/defaults/themes/editorial.yaml +9 -6
- dataface/core/defaults/themes/neon.yaml +15 -8
- dataface/core/defaults/themes/plain.yaml +6 -4
- dataface/core/defaults/themes/stark.yaml +24 -7
- dataface/core/defaults/themes/vivid.yaml +15 -8
- dataface/core/diagnostics/__init__.py +12 -2
- dataface/core/diagnostics/codes_compile.py +17 -33
- dataface/core/diagnostics/codes_execute.py +149 -0
- dataface/core/diagnostics/codes_render.py +144 -31
- dataface/core/diagnostics/diagnostic.py +30 -0
- dataface/core/diagnostics/hints.py +19 -0
- dataface/core/dialects/base.py +17 -0
- dataface/core/dialects/postgres.py +14 -0
- dataface/core/dialects/redshift.py +5 -0
- dataface/core/execute/__init__.py +59 -14
- dataface/core/execute/adapters/__init__.py +1 -1
- dataface/core/execute/adapters/dbt_adapter.py +4 -1
- dataface/core/execute/adapters/dbt_adapter_factory.py +19 -0
- dataface/core/execute/adapters/dbt_utils.py +84 -20
- dataface/core/execute/adapters/postgres_connection_manager.py +48 -0
- dataface/core/execute/adapters/sql_adapter.py +72 -4
- dataface/core/execute/cache_keys.py +4 -9
- dataface/core/execute/dbt_jinja.py +65 -10
- dataface/core/execute/executor.py +94 -19
- dataface/core/execute/file_source_materializer.py +185 -32
- dataface/core/execute/trivial_local_cache.py +5 -2
- dataface/core/font_measure.py +28 -0
- dataface/core/fonts.py +8 -5
- dataface/core/inspect/__init__.py +46 -7
- dataface/core/project.py +19 -3
- dataface/core/render/chart/data_table_attachment.py +14 -12
- dataface/core/render/chart/emitters/_cartesian.py +21 -18
- dataface/core/render/chart/emitters/_channels.py +56 -8
- dataface/core/render/chart/emitters/_label_overlap.py +18 -15
- dataface/core/render/chart/emitters/_measured_label_padding.py +4 -4
- dataface/core/render/chart/emitters/area.py +3 -3
- dataface/core/render/chart/emitters/bar.py +60 -35
- dataface/core/render/chart/emitters/geo.py +8 -13
- dataface/core/render/chart/emitters/heatmap.py +20 -26
- dataface/core/render/chart/emitters/line.py +2 -2
- dataface/core/render/chart/emitters/scatter.py +7 -30
- dataface/core/render/chart/features/baseline.py +22 -9
- dataface/core/render/chart/features/endpoint_labels.py +55 -32
- dataface/core/render/chart/features/mirror_axis.py +12 -9
- dataface/core/render/chart/features/value_labels.py +1 -1
- dataface/core/render/chart/kpi.py +11 -90
- dataface/core/render/chart/mark_extents.py +339 -0
- dataface/core/render/chart/presentation.py +6 -8
- dataface/core/render/chart/rendering.py +100 -46
- dataface/core/render/chart/session.py +10 -0
- dataface/core/render/chart/spec.py +3 -0
- dataface/core/render/chart/spec_builders.py +18 -18
- dataface/core/render/chart/table.py +81 -36
- dataface/core/render/chart/table_support.py +2 -53
- dataface/core/render/chart/time_unit_detect.py +3 -2
- dataface/core/render/chart/translate.py +82 -44
- dataface/core/render/chart/type_inference.py +45 -25
- dataface/core/render/chart/validation.py +91 -0
- dataface/core/render/chart/vega_lite.py +30 -11
- dataface/core/render/chart/vl_field_maps.py +111 -76
- dataface/core/render/conditions.py +122 -0
- dataface/core/render/controls.py +60 -187
- dataface/core/render/converters/chart.py +16 -6
- dataface/core/render/data_format.py +1 -1
- dataface/core/render/face_resolve.py +7 -0
- dataface/core/render/faces.py +97 -32
- dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.woff2 +0 -0
- dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.woff2 +0 -0
- dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -93
- dataface/core/render/fonts/_font_face.css +10 -0
- dataface/core/render/layout_sizing.py +313 -238
- dataface/core/render/numeral_expr.py +80 -0
- dataface/core/render/renderer.py +14 -1
- dataface/core/render/sizing.py +42 -105
- dataface/core/render/svg_utils.py +60 -14
- dataface/core/render/terminal.py +1 -1
- dataface/core/render/variables_layout.py +211 -0
- dataface/core/render/variables_resolve.py +376 -0
- dataface/core/render/variables_strip.py +291 -368
- dataface/core/render/warnings/bar_band_width_too_narrow.py +64 -6
- dataface/core/render/warnings/base.py +18 -0
- dataface/core/render/warnings/layered_chart_shared_y_axis_scale_mismatch.py +25 -3
- dataface/core/render/warnings/layout_min_exceeds_height.py +68 -0
- dataface/core/render/warnings/likely_currency_or_percent_missing_formatter.py +12 -6
- dataface/core/render/warnings/palette_unsupported.py +6 -0
- dataface/core/render/warnings/pie_dominant_segment.py +1 -0
- dataface/core/render/warnings/pie_too_many_segments.py +1 -0
- dataface/core/render/warnings/point_map_out_of_projection.py +1 -0
- dataface/core/render/warnings/query_returned_zero_rows.py +1 -0
- dataface/core/render/warnings/redundant_encoding.py +20 -1
- dataface/core/render/warnings/registry.py +9 -3
- dataface/core/render/warnings/temporal_single_point.py +1 -0
- dataface/core/render/warnings/too_many_color_categories.py +1 -0
- dataface/core/render/warnings/too_many_x_categories.py +18 -2
- dataface/core/render/warnings/value_labels_crowd_width.py +9 -0
- dataface/core/render/warnings/y_encoding_mostly_null.py +1 -0
- dataface/core/render_format.py +18 -0
- dataface/core/serve/alias_index.py +16 -22
- dataface/core/text/format_d3.py +56 -0
- dataface/core/text/numeral_scale.py +218 -0
- dataface/core/utils.py +103 -10
- dataface/data/schemas/board-resolved/board-resolved.schema.json +8589 -7578
- dataface/data/schemas/yaml/0.3.2.json +1 -0
- dataface/data/schemas/yaml/manifest.json +7 -0
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/METADATA +1 -3
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/RECORD +223 -213
- dataface/core/inspect/partials/categorical.yml +0 -38
- dataface/core/inspect/partials/date.yml +0 -38
- dataface/core/inspect/partials/numeric.yml +0 -53
- dataface/core/inspect/templates/charts.yml +0 -94
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/WHEEL +0 -0
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/entry_points.txt +0 -0
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,7 +4,10 @@ core/diagnostics/codes_render.py for what this fires on.
|
|
|
4
4
|
Detection rule:
|
|
5
5
|
chart is a (non-horizontal) ResolvedBarChart AND
|
|
6
6
|
vega_specs[chart_id].encoding.x.type in {"nominal", "ordinal"} AND
|
|
7
|
-
|
|
7
|
+
the estimated per-bar pixel width < _MIN_BAND_WIDTH_PX, where per-bar width
|
|
8
|
+
is vega_specs[chart_id].width / (distinct x values), further divided by the
|
|
9
|
+
series count when the emitted spec subdivides the band with an xOffset
|
|
10
|
+
channel — see _grouped_series_count below.
|
|
8
11
|
|
|
9
12
|
Floor rationale: the theme's default bar border stroke is 1px
|
|
10
13
|
(``marks.bar.border.width``) and the fill occupies ``band_width * 0.8``
|
|
@@ -19,6 +22,7 @@ from __future__ import annotations
|
|
|
19
22
|
|
|
20
23
|
from dataface.core.compile.models.chart.resolved.bar import ResolvedBarChart
|
|
21
24
|
from dataface.core.diagnostics import WARN_BAR_BAND_WIDTH_TOO_NARROW, Diagnostic
|
|
25
|
+
from dataface.core.render.chart._types import VLDict
|
|
22
26
|
from dataface.core.render.warnings.base import WarningContext, encoding_channel_type
|
|
23
27
|
|
|
24
28
|
# Below this per-band pixel width, the bar's fill is unreadable (see rationale above).
|
|
@@ -27,6 +31,57 @@ _MIN_BAND_WIDTH_PX = 4.0
|
|
|
27
31
|
_CATEGORICAL_TYPES = frozenset({"nominal", "ordinal"})
|
|
28
32
|
|
|
29
33
|
|
|
34
|
+
def _grouped_series_count(
|
|
35
|
+
unit: VLDict, chart: ResolvedBarChart, rows: list[VLDict]
|
|
36
|
+
) -> int:
|
|
37
|
+
"""Bars-per-band from the emitter's own offset decision, or 1 if none.
|
|
38
|
+
|
|
39
|
+
The bar emitter (``render/chart/emitters/bar.py``) is the single source
|
|
40
|
+
of truth for whether a band is subdivided — it already accounts for
|
|
41
|
+
stack mode, ``overlap: full``, wide-form grouping (``y`` as a measure
|
|
42
|
+
list), and suppressing the offset channel when color is 1:1 with x (that
|
|
43
|
+
would draw one solo sub-band per category, not a genuine group). Reading
|
|
44
|
+
its emitted ``xOffset`` channel here means this detector reports exactly
|
|
45
|
+
what renders; re-deriving the same "is this grouped" predicate from
|
|
46
|
+
resolved fields drifted from that decision in both directions (false
|
|
47
|
+
positives on colors the emitter doesn't group by, false negatives on
|
|
48
|
+
wide-form measures it does).
|
|
49
|
+
|
|
50
|
+
Small multiples do NOT get a lower count: Vega-Lite resolves the offset
|
|
51
|
+
scale shared across facet panels by default (only ``y`` ever gets
|
|
52
|
+
``resolve.scale: independent``), so a panel facing one series value still
|
|
53
|
+
divides its band across the *global* series domain, occupying a single
|
|
54
|
+
sub-slot — the real render measures sub-pixel bars here, not full-width
|
|
55
|
+
ones. Counting the offset field's cardinality in the chart's unpartitioned
|
|
56
|
+
rows is the correct domain, not an overcount.
|
|
57
|
+
"""
|
|
58
|
+
encoding = unit.get("encoding")
|
|
59
|
+
offset = encoding.get("xOffset") if isinstance(encoding, dict) else None
|
|
60
|
+
if not isinstance(offset, dict):
|
|
61
|
+
return 1
|
|
62
|
+
# A continuous (gradient) offset field isn't a discrete grouping — guard
|
|
63
|
+
# the same way the x-axis categorical check above does.
|
|
64
|
+
if offset.get("type") not in _CATEGORICAL_TYPES:
|
|
65
|
+
return 1
|
|
66
|
+
offset_field = offset.get("field")
|
|
67
|
+
if not isinstance(offset_field, str):
|
|
68
|
+
return 1
|
|
69
|
+
# Wide-form (y as a list of measures) folds into a synthetic label field
|
|
70
|
+
# that Vega-Lite's own `fold` transform creates client-side — it is never
|
|
71
|
+
# present in the query rows, so the series count is just len(chart.y).
|
|
72
|
+
if isinstance(chart.y, list):
|
|
73
|
+
measures = len(chart.y)
|
|
74
|
+
else:
|
|
75
|
+
measures = len({row[offset_field] for row in rows if offset_field in row})
|
|
76
|
+
# A zero count means no series reached this detector at all — an empty
|
|
77
|
+
# result set, or an offset field absent from every row. Treat the band as
|
|
78
|
+
# undivided rather than dividing by zero: this is a diagnostic, and it must
|
|
79
|
+
# never be the thing that fails a render.
|
|
80
|
+
if measures == 0:
|
|
81
|
+
return 1
|
|
82
|
+
return measures
|
|
83
|
+
|
|
84
|
+
|
|
30
85
|
def detect(ctx: WarningContext) -> list[Diagnostic]:
|
|
31
86
|
"""Return one Diagnostic per bar chart whose bands are sub-readable-width."""
|
|
32
87
|
warnings: list[Diagnostic] = []
|
|
@@ -56,26 +111,29 @@ def detect(ctx: WarningContext) -> list[Diagnostic]:
|
|
|
56
111
|
continue
|
|
57
112
|
|
|
58
113
|
x_field: str = chart.x
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
)
|
|
114
|
+
rows = ctx.chart_results[chart_id]
|
|
115
|
+
distinct = len({row[x_field] for row in rows if x_field in row})
|
|
62
116
|
if distinct == 0:
|
|
63
117
|
continue
|
|
64
118
|
|
|
65
119
|
band_width = render_width / distinct
|
|
66
|
-
|
|
120
|
+
series = _grouped_series_count(unit, chart, rows)
|
|
121
|
+
bar_width = band_width / series
|
|
122
|
+
if bar_width >= _MIN_BAND_WIDTH_PX:
|
|
67
123
|
continue
|
|
68
124
|
|
|
69
125
|
warnings.append(
|
|
70
126
|
Diagnostic.from_code(
|
|
71
127
|
WARN_BAR_BAND_WIDTH_TOO_NARROW,
|
|
72
128
|
chart=chart_id,
|
|
129
|
+
path=f"charts.{chart_id}.x",
|
|
73
130
|
field=x_field,
|
|
74
131
|
message=WARN_BAR_BAND_WIDTH_TOO_NARROW.message_template.format(
|
|
75
132
|
chart_id=chart_id,
|
|
76
133
|
distinct=distinct,
|
|
134
|
+
series=series,
|
|
77
135
|
render_width=render_width,
|
|
78
|
-
|
|
136
|
+
bar_width=bar_width,
|
|
79
137
|
min_band_width=_MIN_BAND_WIDTH_PX,
|
|
80
138
|
),
|
|
81
139
|
fix=WARN_BAR_BAND_WIDTH_TOO_NARROW.fix_template,
|
|
@@ -9,6 +9,8 @@ WarningContext carries everything detectors need:
|
|
|
9
9
|
- chart_results: chart id → list of row dicts (query output).
|
|
10
10
|
- vega_specs: chart id → Vega-Lite spec dict.
|
|
11
11
|
- table_overflows: chart id → the table's slot overflow captured at render.
|
|
12
|
+
- authored_chart_heights: chart id → the nearest ancestor's literal
|
|
13
|
+
authored layout height, in px.
|
|
12
14
|
|
|
13
15
|
vega_specs is SPARSE: KPI, text, and markdown charts do not compile to
|
|
14
16
|
Vega-Lite, so their ids are OMITTED from this dict (not present as None).
|
|
@@ -19,6 +21,18 @@ Detectors that key into vega_specs must guard with:
|
|
|
19
21
|
table_overflows is likewise SPARSE: only tables that overflowed their slot
|
|
20
22
|
appear, and only on SVG-family renders (non-SVG formats never rasterize a
|
|
21
23
|
table, so it is empty for them).
|
|
24
|
+
|
|
25
|
+
authored_chart_heights is likewise SPARSE: a chart with no authored height
|
|
26
|
+
anywhere in its ancestor chain is OMITTED, not present as 0 or None. The
|
|
27
|
+
value is a snapshot of the chart's real, assigned px slot height, taken by
|
|
28
|
+
build_resolved_face (via layout_sizing._snapshot_authored_slot_heights)
|
|
29
|
+
immediately after the sizing pass assigns it but before cols-alignment can
|
|
30
|
+
re-expand it — for a `cols:`-wrapped chart, that alignment step re-expands
|
|
31
|
+
the resolved height to the chart's natural/unconstrained size afterward to
|
|
32
|
+
keep siblings visually aligned, discarding the authored cap. Because the
|
|
33
|
+
value is read off the sizing pass's own output (not re-derived), it already
|
|
34
|
+
reflects a `rows:` wrapper splitting its height across children and a
|
|
35
|
+
percentage-authored height resolved against real available space.
|
|
22
36
|
"""
|
|
23
37
|
|
|
24
38
|
from __future__ import annotations
|
|
@@ -42,6 +56,10 @@ class WarningContext(BaseModel):
|
|
|
42
56
|
# chart id → the table's slot overflow at render time (tables only; sparse —
|
|
43
57
|
# captured from the real SVG render, so empty for non-SVG formats).
|
|
44
58
|
table_overflows: dict[str, TableOverflow] = {}
|
|
59
|
+
# chart id → nearest ancestor's literal authored layout height, in px
|
|
60
|
+
# (sparse — see module docstring for why this isn't derived from
|
|
61
|
+
# face_spec.layout's resolved .height).
|
|
62
|
+
authored_chart_heights: dict[str, float] = {}
|
|
45
63
|
|
|
46
64
|
|
|
47
65
|
def encoding_channel_type(vega_spec: dict[str, object], channel: str) -> str:
|
|
@@ -13,7 +13,9 @@ Detection rule:
|
|
|
13
13
|
having axis_y means the user has already opted into split scales)
|
|
14
14
|
|
|
15
15
|
Diagnostic.field is None — the issue is cross-column, not column-scoped.
|
|
16
|
-
The message names the two columns with the widest ratio
|
|
16
|
+
The message names the two columns with the widest ratio, and the diagnostic
|
|
17
|
+
marks where each was authored: `path` on the larger series, `related` on the
|
|
18
|
+
smaller one that gets crushed.
|
|
17
19
|
"""
|
|
18
20
|
|
|
19
21
|
from __future__ import annotations
|
|
@@ -24,6 +26,7 @@ from dataface.core.compile.models.chart.resolved._layer import LayeredResolvedCh
|
|
|
24
26
|
from dataface.core.diagnostics import (
|
|
25
27
|
WARN_LAYERED_CHART_SHARED_Y_AXIS_SCALE_MISMATCH,
|
|
26
28
|
Diagnostic,
|
|
29
|
+
RelatedLocation,
|
|
27
30
|
)
|
|
28
31
|
from dataface.core.render.warnings.base import WarningContext
|
|
29
32
|
|
|
@@ -54,14 +57,19 @@ def detect(ctx: WarningContext) -> list[Diagnostic]:
|
|
|
54
57
|
continue
|
|
55
58
|
|
|
56
59
|
# Collect (column_name, absolute_median, axis_y_set) for the base
|
|
57
|
-
# chart's own y column plus every layer's y column.
|
|
60
|
+
# chart's own y column plus every layer's y column. The authored path
|
|
61
|
+
# each column came from rides along so the two extremes can be marked
|
|
62
|
+
# where they were actually written.
|
|
58
63
|
column_axis_y: list[tuple[str, bool]] = []
|
|
64
|
+
column_paths: dict[str, str] = {}
|
|
59
65
|
base_y = chart.y
|
|
60
66
|
if isinstance(base_y, str):
|
|
61
67
|
column_axis_y.append((base_y, False))
|
|
62
|
-
|
|
68
|
+
column_paths[base_y] = f"charts.{chart_id}.y"
|
|
69
|
+
for idx, layer in enumerate(chart.layers):
|
|
63
70
|
if layer.y is not None:
|
|
64
71
|
column_axis_y.append((layer.y, layer.axis_y.position is not None))
|
|
72
|
+
column_paths.setdefault(layer.y, f"charts.{chart_id}.layers.{idx}.y")
|
|
65
73
|
|
|
66
74
|
layer_stats: list[tuple[str, float, bool]] = []
|
|
67
75
|
seen_columns: set[str] = set()
|
|
@@ -101,11 +109,25 @@ def detect(ctx: WarningContext) -> list[Diagnostic]:
|
|
|
101
109
|
if max_axis_y or min_axis_y:
|
|
102
110
|
continue
|
|
103
111
|
|
|
112
|
+
# The complaint is the *ratio between two series*, so it is anchored on
|
|
113
|
+
# the larger one and carries the smaller — the one being crushed to a
|
|
114
|
+
# flat line — as a related location. Marking only one of the pair, or
|
|
115
|
+
# the whole chart block, loses which two columns are at odds.
|
|
104
116
|
warnings.append(
|
|
105
117
|
Diagnostic.from_code(
|
|
106
118
|
WARN_LAYERED_CHART_SHARED_Y_AXIS_SCALE_MISMATCH,
|
|
107
119
|
chart=chart_id,
|
|
108
120
|
field=None,
|
|
121
|
+
path=column_paths[max_col],
|
|
122
|
+
related=(
|
|
123
|
+
RelatedLocation(
|
|
124
|
+
path=column_paths[min_col],
|
|
125
|
+
message=(
|
|
126
|
+
f"{min_col!r} is ~{ratio:.0f}x smaller — this is the "
|
|
127
|
+
"series that flattens"
|
|
128
|
+
),
|
|
129
|
+
),
|
|
130
|
+
),
|
|
109
131
|
message=WARN_LAYERED_CHART_SHARED_Y_AXIS_SCALE_MISMATCH.message_template.format(
|
|
110
132
|
chart_id=chart_id, col_a=max_col, col_b=min_col, ratio=ratio
|
|
111
133
|
),
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Detector: WARN_LAYOUT_MIN_EXCEEDS_HEIGHT — see its `doc` in
|
|
2
|
+
core/diagnostics/codes_render.py for what this fires on.
|
|
3
|
+
|
|
4
|
+
Detection rule:
|
|
5
|
+
chart is a horizontal ResolvedBarChart AND
|
|
6
|
+
it has a literal authored height somewhere in its ancestor chain
|
|
7
|
+
(ctx.authored_chart_heights) AND
|
|
8
|
+
min_height_for_horizontal_bar_categories(n, ...) — the same readability
|
|
9
|
+
floor vega_lite.py's _render_vl_artifact applies at render time — exceeds
|
|
10
|
+
that authored height.
|
|
11
|
+
|
|
12
|
+
The authored height comes from ctx.authored_chart_heights, a snapshot of
|
|
13
|
+
each chart's real, assigned px slot height taken while it's still intact —
|
|
14
|
+
after the sizing pass assigns it, before cols-alignment can re-expand it.
|
|
15
|
+
See layout_sizing.py's `_snapshot_authored_slot_heights` for the full
|
|
16
|
+
mechanism.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from dataface.core.compile.models.chart.resolved.bar import ResolvedBarChart
|
|
22
|
+
from dataface.core.diagnostics import WARN_LAYOUT_MIN_EXCEEDS_HEIGHT, Diagnostic
|
|
23
|
+
from dataface.core.render.chart.emitters._cartesian import (
|
|
24
|
+
count_horizontal_bar_categories,
|
|
25
|
+
min_height_for_horizontal_bar_categories,
|
|
26
|
+
)
|
|
27
|
+
from dataface.core.render.warnings.base import WarningContext
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def detect(ctx: WarningContext) -> list[Diagnostic]:
|
|
31
|
+
"""Return one Diagnostic per horizontal bar whose category floor exceeds
|
|
32
|
+
its nearest ancestor's authored height."""
|
|
33
|
+
warnings: list[Diagnostic] = []
|
|
34
|
+
|
|
35
|
+
for chart_id, chart in ctx.face_spec.charts.items():
|
|
36
|
+
if not isinstance(chart, ResolvedBarChart):
|
|
37
|
+
continue
|
|
38
|
+
if chart.orientation != "horizontal":
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
authored_height = ctx.authored_chart_heights.get(chart_id)
|
|
42
|
+
if authored_height is None:
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
rows = ctx.chart_results.get(chart_id)
|
|
46
|
+
if not rows:
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
n = count_horizontal_bar_categories(chart.x, rows)
|
|
50
|
+
min_h = min_height_for_horizontal_bar_categories(n, ctx.face_spec.style.charts)
|
|
51
|
+
if min_h <= authored_height:
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
warnings.append(
|
|
55
|
+
Diagnostic.from_code(
|
|
56
|
+
WARN_LAYOUT_MIN_EXCEEDS_HEIGHT,
|
|
57
|
+
chart=chart_id,
|
|
58
|
+
message=WARN_LAYOUT_MIN_EXCEEDS_HEIGHT.message_template.format(
|
|
59
|
+
chart_id=chart_id,
|
|
60
|
+
n_categories=n,
|
|
61
|
+
min_height=min_h,
|
|
62
|
+
authored_height=authored_height,
|
|
63
|
+
),
|
|
64
|
+
fix=WARN_LAYOUT_MIN_EXCEEDS_HEIGHT.fix_template,
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return warnings
|
|
@@ -13,13 +13,13 @@ Detection rule (v1, name-match only):
|
|
|
13
13
|
or contains the substring: percent.
|
|
14
14
|
|
|
15
15
|
Fire when a matching field's effective y-axis format does not suit its kind.
|
|
16
|
-
Every format source (chart-root ``format:``, authored ``style.axis_y.format``,
|
|
16
|
+
Every format source (chart-root ``format:``, authored ``style.axis_y.labels.format``,
|
|
17
17
|
whole-chart ``style.number_format``, the theme default) is baked into the single
|
|
18
|
-
resolved ``style.axis_y.format`` by the time this detector runs, so it reads
|
|
18
|
+
resolved ``style.axis_y.labels.format`` by the time this detector runs, so it reads
|
|
19
19
|
that one value rather than trying to recover authored-vs-default intent. A
|
|
20
20
|
percent format carries ``%``; a currency format carries ``$``; the generic SI
|
|
21
21
|
default (``.3~s``) suits neither.
|
|
22
|
-
- Cartesian charts: check the chart's ``style.axis_y.format``.
|
|
22
|
+
- Cartesian charts: check the chart's ``style.axis_y.labels.format``.
|
|
23
23
|
- Layered charts share one y-axis, so its format is checked once per layer;
|
|
24
24
|
a warning is emitted per layer whose y field matches a signal and whose
|
|
25
25
|
shared format is unfit.
|
|
@@ -108,9 +108,9 @@ def _classify(field_name: str) -> str | None:
|
|
|
108
108
|
def _format_suits_kind(fmt: str | None, kind: str) -> bool:
|
|
109
109
|
"""Whether a resolved d3 number-format string renders ``kind`` sanely.
|
|
110
110
|
|
|
111
|
-
Every format source (chart-root ``format:``, authored ``style.axis_y.format``,
|
|
111
|
+
Every format source (chart-root ``format:``, authored ``style.axis_y.labels.format``,
|
|
112
112
|
the theme default, and whole-chart ``style.number_format``) is baked into the
|
|
113
|
-
single ``style.axis_y.format`` by the resolved boundary, so authored-vs-default
|
|
113
|
+
single ``style.axis_y.labels.format`` by the resolved boundary, so authored-vs-default
|
|
114
114
|
can't be read off the resolved chart — but the effective format can. The
|
|
115
115
|
generic SI default (``.3~s``) suits neither kind: a 0–1 percent renders as
|
|
116
116
|
milli-units and currency loses its symbol. A percent format carries ``%``; a
|
|
@@ -125,6 +125,12 @@ def _warn(chart_id: str, field: str, kind: str, axis_format: str | None) -> Diag
|
|
|
125
125
|
return Diagnostic.from_code(
|
|
126
126
|
WARN_LIKELY_CURRENCY_OR_PERCENT_MISSING_FORMATTER,
|
|
127
127
|
chart=chart_id,
|
|
128
|
+
# `format` lives on `axis_y.labels`, not on `axis_y` — the bare
|
|
129
|
+
# `axis_y.format` spelling is pre-0.3.2 and now emits a migration
|
|
130
|
+
# warning. This is the spelling `fix_template` tells authors to set.
|
|
131
|
+
# Usually an *absent* key (that is the complaint), so it leans on the
|
|
132
|
+
# candidate walk to land on `axis_y`, then `style`, then the chart.
|
|
133
|
+
path=f"charts.{chart_id}.style.axis_y.labels.format",
|
|
128
134
|
field=field,
|
|
129
135
|
message=WARN_LIKELY_CURRENCY_OR_PERCENT_MISSING_FORMATTER.message_template.format(
|
|
130
136
|
chart_id=chart_id, field=field, kind=kind, format=axis_format
|
|
@@ -154,7 +160,7 @@ def _y_axis_check(chart: ResolvedChart) -> tuple[str | None, list[str]]:
|
|
|
154
160
|
fields.extend(layer.y for layer in chart.layers if layer.y)
|
|
155
161
|
if not fields:
|
|
156
162
|
return None, []
|
|
157
|
-
return chart.style.axis_y.format, fields
|
|
163
|
+
return chart.style.axis_y.labels.format, fields
|
|
158
164
|
|
|
159
165
|
|
|
160
166
|
def detect(ctx: WarningContext) -> list[Diagnostic]:
|
|
@@ -30,6 +30,12 @@ def detect(ctx: WarningContext) -> list[Diagnostic]:
|
|
|
30
30
|
Diagnostic.from_code(
|
|
31
31
|
WARN_PALETTE_UNSUPPORTED,
|
|
32
32
|
chart=chart_id,
|
|
33
|
+
# `requested_alias_palette` is set on CategoricalColorStyle, so
|
|
34
|
+
# the authored key is under `style.color.categorical` — either
|
|
35
|
+
# `palette` or `single_series_palette`. Naming the common one
|
|
36
|
+
# walks up one level for the other, which is still the right
|
|
37
|
+
# block. There is no `scale` node on ColorStyle.
|
|
38
|
+
path=f"charts.{chart_id}.style.color.categorical.palette",
|
|
33
39
|
field="palette",
|
|
34
40
|
message=WARN_PALETTE_UNSUPPORTED.message_template.format(
|
|
35
41
|
chart_id=chart_id,
|
|
@@ -50,6 +50,7 @@ def detect(ctx: WarningContext) -> list[Diagnostic]:
|
|
|
50
50
|
Diagnostic.from_code(
|
|
51
51
|
WARN_PIE_DOMINANT_SEGMENT,
|
|
52
52
|
chart=chart_id,
|
|
53
|
+
path=f"charts.{chart_id}.type",
|
|
53
54
|
field=theta_field,
|
|
54
55
|
message=WARN_PIE_DOMINANT_SEGMENT.message_template.format(
|
|
55
56
|
chart_id=chart_id, dominant_field=theta_field, dominant_share=share
|
|
@@ -33,6 +33,7 @@ def detect(ctx: WarningContext) -> list[Diagnostic]:
|
|
|
33
33
|
Diagnostic.from_code(
|
|
34
34
|
WARN_PIE_TOO_MANY_SEGMENTS,
|
|
35
35
|
chart=chart_id,
|
|
36
|
+
path=f"charts.{chart_id}.type",
|
|
36
37
|
message=WARN_PIE_TOO_MANY_SEGMENTS.message_template.format(
|
|
37
38
|
chart_id=chart_id,
|
|
38
39
|
segment_count=segment_count,
|
|
@@ -63,6 +63,7 @@ def detect(ctx: WarningContext) -> list[Diagnostic]:
|
|
|
63
63
|
Diagnostic.from_code(
|
|
64
64
|
WARN_POINT_MAP_OUT_OF_PROJECTION,
|
|
65
65
|
chart=chart_id,
|
|
66
|
+
path=f"charts.{chart_id}.projection",
|
|
66
67
|
message=WARN_POINT_MAP_OUT_OF_PROJECTION.message_template.format(
|
|
67
68
|
chart_id=chart_id,
|
|
68
69
|
dropped_count=out_count,
|
|
@@ -29,6 +29,7 @@ def detect(ctx: WarningContext) -> list[Diagnostic]:
|
|
|
29
29
|
Diagnostic.from_code(
|
|
30
30
|
WARN_QUERY_RETURNED_ZERO_ROWS,
|
|
31
31
|
chart=chart_id,
|
|
32
|
+
path=f"charts.{chart_id}.query",
|
|
32
33
|
field=None,
|
|
33
34
|
message=WARN_QUERY_RETURNED_ZERO_ROWS.message_template.format(
|
|
34
35
|
chart_id=chart_id
|
|
@@ -20,7 +20,11 @@ from dataface.core.compile.models.chart.resolved._base import (
|
|
|
20
20
|
)
|
|
21
21
|
from dataface.core.compile.models.chart.resolved.pie import ResolvedPieChart
|
|
22
22
|
from dataface.core.compile.models.chart.resolved.scatter import ResolvedScatterChart
|
|
23
|
-
from dataface.core.diagnostics import
|
|
23
|
+
from dataface.core.diagnostics import (
|
|
24
|
+
WARN_REDUNDANT_ENCODING,
|
|
25
|
+
Diagnostic,
|
|
26
|
+
RelatedLocation,
|
|
27
|
+
)
|
|
24
28
|
from dataface.core.render.warnings.base import WarningContext
|
|
25
29
|
|
|
26
30
|
|
|
@@ -62,11 +66,26 @@ def detect(ctx: WarningContext) -> list[Diagnostic]:
|
|
|
62
66
|
if chart.chart_type == "bar" and set(channels) == {"x", "color"}:
|
|
63
67
|
continue
|
|
64
68
|
channel_list = ", ".join(sorted(channels))
|
|
69
|
+
# The complaint is about the *pair*, so marking one channel alone
|
|
70
|
+
# tells half of it. Anchor the first and carry every other one as a
|
|
71
|
+
# related location, each labelled with the channel it is.
|
|
72
|
+
first, *rest = sorted(channels)
|
|
65
73
|
warnings.append(
|
|
66
74
|
Diagnostic.from_code(
|
|
67
75
|
WARN_REDUNDANT_ENCODING,
|
|
68
76
|
chart=chart_id,
|
|
69
77
|
field=field,
|
|
78
|
+
path=f"charts.{chart_id}.{first}",
|
|
79
|
+
related=tuple(
|
|
80
|
+
RelatedLocation(
|
|
81
|
+
path=f"charts.{chart_id}.{channel}",
|
|
82
|
+
message=(
|
|
83
|
+
f"also bound to {field!r} — this is the "
|
|
84
|
+
"redundant binding"
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
for channel in rest
|
|
88
|
+
),
|
|
70
89
|
message=WARN_REDUNDANT_ENCODING.message_template.format(
|
|
71
90
|
chart_id=chart_id, field=field, channels=channel_list
|
|
72
91
|
),
|
|
@@ -15,6 +15,7 @@ from types import ModuleType
|
|
|
15
15
|
|
|
16
16
|
import dataface.core.render.warnings.bar_band_width_too_narrow as bar_band_width_too_narrow
|
|
17
17
|
import dataface.core.render.warnings.layered_chart_shared_y_axis_scale_mismatch as layered_chart_shared_y_axis_scale_mismatch
|
|
18
|
+
import dataface.core.render.warnings.layout_min_exceeds_height as layout_min_exceeds_height
|
|
18
19
|
import dataface.core.render.warnings.likely_currency_or_percent_missing_formatter as likely_currency_or_percent_missing_formatter
|
|
19
20
|
import dataface.core.render.warnings.palette_unsupported as palette_unsupported
|
|
20
21
|
import dataface.core.render.warnings.pie_dominant_segment as pie_dominant_segment
|
|
@@ -36,6 +37,7 @@ logger = logging.getLogger(__name__)
|
|
|
36
37
|
DETECTORS: list[ModuleType] = [
|
|
37
38
|
bar_band_width_too_narrow,
|
|
38
39
|
layered_chart_shared_y_axis_scale_mismatch,
|
|
40
|
+
layout_min_exceeds_height,
|
|
39
41
|
likely_currency_or_percent_missing_formatter,
|
|
40
42
|
palette_unsupported,
|
|
41
43
|
pie_dominant_segment,
|
|
@@ -64,9 +66,13 @@ def run_all(ctx: WarningContext) -> list[Diagnostic]:
|
|
|
64
66
|
results.extend(detector.detect(ctx))
|
|
65
67
|
except Exception: # noqa: BLE001 — detector failures must not break renders
|
|
66
68
|
logger.exception("Warning detector %r raised; skipping", detector)
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
69
|
+
# A detector that knows which authored key its complaint is about composes
|
|
70
|
+
# its own `charts.<id>.<anchor>` path, so the squiggle lands on that field
|
|
71
|
+
# rather than the whole chart block. The fallback below is for the
|
|
72
|
+
# detectors that genuinely have nothing more specific to say — an
|
|
73
|
+
# unreferenced chart, a table too wide for its slot — and it keeps a new
|
|
74
|
+
# detector stampable with zero position wiring: stamping `.chart`, which it
|
|
75
|
+
# already does, is enough.
|
|
70
76
|
for d in results:
|
|
71
77
|
if d.path is None and d.chart is not None:
|
|
72
78
|
d.path = f"charts.{d.chart}"
|
|
@@ -48,6 +48,7 @@ def detect(ctx: WarningContext) -> list[Diagnostic]:
|
|
|
48
48
|
Diagnostic.from_code(
|
|
49
49
|
WARN_TEMPORAL_SINGLE_POINT,
|
|
50
50
|
chart=chart_id,
|
|
51
|
+
path=f"charts.{chart_id}.x",
|
|
51
52
|
field=chart.x,
|
|
52
53
|
message=WARN_TEMPORAL_SINGLE_POINT.message_template.format(
|
|
53
54
|
chart_id=chart_id, chart_type=chart.chart_type
|
|
@@ -47,6 +47,7 @@ def detect(ctx: WarningContext) -> list[Diagnostic]:
|
|
|
47
47
|
Diagnostic.from_code(
|
|
48
48
|
WARN_TOO_MANY_COLOR_CATEGORIES,
|
|
49
49
|
chart=chart_id,
|
|
50
|
+
path=f"charts.{chart_id}.color",
|
|
50
51
|
field=color_field,
|
|
51
52
|
message=WARN_TOO_MANY_COLOR_CATEGORIES.message_template.format(
|
|
52
53
|
chart_id=chart_id,
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
core/diagnostics/codes_render.py for what this fires on.
|
|
3
3
|
|
|
4
4
|
Detection rule:
|
|
5
|
-
vega_specs[chart_id].encoding.x.type in {"nominal", "ordinal"}
|
|
5
|
+
vega_specs[chart_id].encoding.x.type in {"nominal", "ordinal"} (or, for a
|
|
6
|
+
bar chart, also "temporal" — see _BAR_ALLOWED_X_TYPES below)
|
|
6
7
|
AND distinct x values in chart_results > _MAX_CATEGORIES
|
|
7
8
|
"""
|
|
8
9
|
|
|
@@ -11,6 +12,7 @@ from __future__ import annotations
|
|
|
11
12
|
from dataface.core.compile.models.chart.resolved._base import (
|
|
12
13
|
_CartesianResolvedChartFields,
|
|
13
14
|
)
|
|
15
|
+
from dataface.core.compile.models.chart.resolved.bar import ResolvedBarChart
|
|
14
16
|
from dataface.core.diagnostics import WARN_TOO_MANY_X_CATEGORIES, Diagnostic
|
|
15
17
|
from dataface.core.render.warnings.base import WarningContext, encoding_channel_type
|
|
16
18
|
|
|
@@ -19,6 +21,14 @@ _MAX_CATEGORIES = 50
|
|
|
19
21
|
|
|
20
22
|
_CATEGORICAL_TYPES = frozenset({"nominal", "ordinal"})
|
|
21
23
|
|
|
24
|
+
# A bar draws one band per distinct x value regardless of whether Vega-Lite
|
|
25
|
+
# encodes that axis as ordinal or temporal — the density gate that flips
|
|
26
|
+
# bucketed temporal data off ordinal above ~60 distinct values does so purely
|
|
27
|
+
# to manage axis-label layout, not because the chart stopped being banded.
|
|
28
|
+
# Line/area/scatter charts don't get this widening: a dense temporal axis
|
|
29
|
+
# there is a continuous draw, not a crowded band, however many points it has.
|
|
30
|
+
_BAR_ALLOWED_X_TYPES = _CATEGORICAL_TYPES | frozenset({"temporal"})
|
|
31
|
+
|
|
22
32
|
|
|
23
33
|
def detect(ctx: WarningContext) -> list[Diagnostic]:
|
|
24
34
|
"""Return one Diagnostic per chart with an overcrowded categorical x-axis."""
|
|
@@ -32,8 +42,13 @@ def detect(ctx: WarningContext) -> list[Diagnostic]:
|
|
|
32
42
|
if chart_id not in ctx.vega_specs or chart_id not in ctx.chart_results:
|
|
33
43
|
continue
|
|
34
44
|
|
|
45
|
+
allowed_x_types = (
|
|
46
|
+
_BAR_ALLOWED_X_TYPES
|
|
47
|
+
if isinstance(chart, ResolvedBarChart)
|
|
48
|
+
else _CATEGORICAL_TYPES
|
|
49
|
+
)
|
|
35
50
|
x_type = encoding_channel_type(ctx.vega_specs[chart_id], "x")
|
|
36
|
-
if x_type not in
|
|
51
|
+
if x_type not in allowed_x_types:
|
|
37
52
|
continue
|
|
38
53
|
|
|
39
54
|
x_field: str = chart.x
|
|
@@ -47,6 +62,7 @@ def detect(ctx: WarningContext) -> list[Diagnostic]:
|
|
|
47
62
|
Diagnostic.from_code(
|
|
48
63
|
WARN_TOO_MANY_X_CATEGORIES,
|
|
49
64
|
chart=chart_id,
|
|
65
|
+
path=f"charts.{chart_id}.x",
|
|
50
66
|
field=x_field,
|
|
51
67
|
message=WARN_TOO_MANY_X_CATEGORIES.message_template.format(
|
|
52
68
|
chart_id=chart_id,
|
|
@@ -41,12 +41,18 @@ def detect(ctx: WarningContext) -> list[Diagnostic]:
|
|
|
41
41
|
# length (a different axis) and are out of scope; area labels ride the
|
|
42
42
|
# overlaid line mark.
|
|
43
43
|
labels: MarkLabelsStyle
|
|
44
|
+
# `mark_key` is the authored `marks.<key>` the labels came from, so the
|
|
45
|
+
# warning marks where they were switched on. Area labels ride the
|
|
46
|
+
# overlaid *line* mark, which is why it is not always the family name.
|
|
47
|
+
mark_key: str
|
|
44
48
|
if isinstance(chart, ResolvedBarChart):
|
|
45
49
|
if chart.orientation == "horizontal":
|
|
46
50
|
continue
|
|
47
51
|
labels = chart.style.mark.labels
|
|
52
|
+
mark_key = "bar"
|
|
48
53
|
elif isinstance(chart, (ResolvedLineChart, ResolvedAreaChart)):
|
|
49
54
|
labels = chart.style.line_mark.labels
|
|
55
|
+
mark_key = "line"
|
|
50
56
|
else:
|
|
51
57
|
continue
|
|
52
58
|
|
|
@@ -109,6 +115,9 @@ def detect(ctx: WarningContext) -> list[Diagnostic]:
|
|
|
109
115
|
Diagnostic.from_code(
|
|
110
116
|
WARN_VALUE_LABELS_CROWD_WIDTH,
|
|
111
117
|
chart=chart_id,
|
|
118
|
+
# Chart-local style takes no family segment — `style.bar.…` is
|
|
119
|
+
# the board-level spelling and does not compile on a chart.
|
|
120
|
+
path=f"charts.{chart_id}.style.marks.{mark_key}.labels",
|
|
112
121
|
field=label_field,
|
|
113
122
|
message=WARN_VALUE_LABELS_CROWD_WIDTH.message_template.format(
|
|
114
123
|
chart_id=chart_id, label_width=widest, slot_width=slot
|
|
@@ -54,6 +54,7 @@ def detect(ctx: WarningContext) -> list[Diagnostic]:
|
|
|
54
54
|
Diagnostic.from_code(
|
|
55
55
|
WARN_Y_ENCODING_MOSTLY_NULL,
|
|
56
56
|
chart=chart_id,
|
|
57
|
+
path=f"charts.{chart_id}.y",
|
|
57
58
|
field=y_field,
|
|
58
59
|
message=WARN_Y_ENCODING_MOSTLY_NULL.message_template.format(
|
|
59
60
|
chart_id=chart_id,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""The `RenderFormat` type alias, split out of `dataface.core.dashboard`.
|
|
2
|
+
|
|
3
|
+
`RenderFormat` is pure type metadata with no runtime logic — but `dashboard.py`
|
|
4
|
+
(where it used to live) is the compile+execute+render orchestrator and eagerly
|
|
5
|
+
imports the entire `core.compile` package. CLI/MCP surfaces that need
|
|
6
|
+
`RenderFormat` purely for argument typing (e.g. Typer's `--format` option,
|
|
7
|
+
which needs the real `Literal` object at decoration time, not just an
|
|
8
|
+
annotation) were paying that whole cost just to get this one type. Living
|
|
9
|
+
here, they don't.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Literal
|
|
15
|
+
|
|
16
|
+
RenderFormat = Literal[
|
|
17
|
+
"svg", "html", "png", "pdf", "terminal", "json", "text", "yaml", "data"
|
|
18
|
+
]
|