dataface 0.3.2.dev22__py3-none-any.whl → 0.3.2.dev111__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dataface/__init__.py +70 -23
- dataface/agent_api/__init__.py +162 -68
- dataface/agent_api/_paths.py +9 -3
- dataface/agent_api/docs/yaml-reference.md +240 -106
- dataface/agent_api/init.py +1 -1
- dataface/agent_api/project_session.py +2 -1
- dataface/agent_api/serve.py +115 -0
- dataface/ai/agent.py +28 -2
- dataface/cli/_project.py +100 -1
- dataface/cli/commands/board_artifact.py +5 -2
- dataface/cli/commands/describe.py +7 -3
- dataface/cli/commands/docs.py +12 -3
- dataface/cli/commands/init.py +7 -14
- dataface/cli/commands/inspect.py +6 -1
- dataface/cli/commands/mcp_init.py +23 -43
- dataface/cli/commands/migrate.py +8 -3
- dataface/cli/commands/query.py +11 -8
- dataface/cli/commands/render.py +20 -9
- dataface/cli/commands/search.py +8 -2
- dataface/cli/commands/serve.py +26 -76
- dataface/cli/commands/skills_init.py +2 -19
- dataface/cli/commands/validate.py +7 -2
- dataface/cli/filesystem_project.py +5 -4
- dataface/cli/main.py +2 -19
- dataface/core/__init__.py +73 -28
- dataface/core/{compile/colors.py → colors.py} +20 -5
- dataface/core/compile/compiler.py +97 -34
- dataface/core/compile/data_table.py +7 -2
- dataface/core/compile/enrich.py +18 -44
- dataface/core/compile/face_warnings.py +46 -8
- dataface/core/compile/format.py +47 -1
- dataface/core/compile/inherit_graph.py +3 -3
- dataface/core/compile/inherit_registry.yaml +1 -1
- dataface/core/compile/migrations.py +112 -9
- dataface/core/compile/models/chart/normalized/kpi.py +0 -4
- dataface/core/compile/models/chart/resolved/_base.py +28 -10
- dataface/core/compile/models/chart/resolved/callout.py +2 -3
- dataface/core/compile/models/chart/resolved/kpi.py +7 -3
- dataface/core/compile/models/chart/resolved/table.py +16 -2
- dataface/core/compile/models/markers.py +1 -1
- dataface/core/compile/models/primitives.py +175 -2
- dataface/core/compile/models/source.py +5 -5
- dataface/core/compile/models/style/authored/__init__.py +32 -8
- dataface/core/compile/models/style/authored/_base.py +126 -69
- dataface/core/compile/models/style/authored/table.py +61 -3
- dataface/core/compile/models/style/resolved/__init__.py +12 -2
- dataface/core/compile/models/style/resolved/_base.py +142 -57
- dataface/core/compile/models/style/resolved/_cartesian.py +6 -11
- dataface/core/compile/models/style/resolved/_cascade.py +141 -54
- dataface/core/compile/models/style/resolved/area.py +1 -1
- dataface/core/compile/models/style/resolved/bar.py +1 -1
- dataface/core/compile/models/style/resolved/line.py +1 -1
- dataface/core/compile/models/style/resolved/scatter.py +3 -2
- dataface/core/compile/models/style/theme/__init__.py +36 -14
- dataface/core/compile/models/style/theme/_chart_base.py +17 -9
- dataface/core/compile/models/style/theme/axis.py +432 -312
- dataface/core/compile/models/style/theme/charts.py +10 -6
- dataface/core/compile/models/style/theme/kpi.py +2 -1
- dataface/core/compile/models/style/theme/page.py +5 -2
- dataface/core/compile/models/style/theme/table.py +2 -2
- dataface/core/compile/models/vega_lite/config.py +1 -1
- dataface/core/compile/normalize_charts.py +6 -2
- dataface/core/compile/normalize_queries.py +21 -7
- dataface/core/compile/resolve/__init__.py +2 -0
- dataface/core/compile/resolve/_axes.py +147 -44
- dataface/core/compile/resolve/_channels.py +1 -1
- dataface/core/compile/resolve/_dispatch.py +1 -1
- dataface/core/compile/resolve/_domain.py +49 -32
- dataface/core/compile/resolve/_kwargs.py +39 -12
- dataface/core/compile/resolve/_marks.py +8 -3
- dataface/core/compile/resolve/_palette.py +7 -3
- dataface/core/compile/resolve/_table.py +124 -33
- dataface/core/compile/resolve/area.py +48 -18
- dataface/core/compile/resolve/bar.py +245 -64
- dataface/core/compile/resolve/geo.py +10 -3
- dataface/core/compile/resolve/heatmap.py +16 -8
- dataface/core/compile/resolve/line.py +35 -12
- dataface/core/compile/resolve/pie.py +4 -1
- dataface/core/compile/resolve/pie_attachment.py +1 -1
- dataface/core/compile/resolve/scatter.py +25 -13
- dataface/core/compile/resolve/simple.py +25 -2
- dataface/core/compile/schema_renderers/prompt.py +1 -1
- dataface/core/compile/schema_renderers/textmate_grammar.py +8 -1
- dataface/core/compile/source_map.py +211 -28
- dataface/core/compile/style_cascade.py +126 -55
- dataface/core/compile/yaml_error_formatter.py +1 -1
- dataface/core/connections.py +10 -3
- dataface/core/dashboard.py +24 -18
- dataface/core/defaults/themes/_base.yaml +49 -39
- dataface/core/defaults/themes/cream.yaml +4 -4
- dataface/core/defaults/themes/diagnostics-title-baseline.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +1 -1
- dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +1 -1
- dataface/core/defaults/themes/editorial.yaml +6 -6
- dataface/core/defaults/themes/neon.yaml +15 -8
- dataface/core/defaults/themes/plain.yaml +6 -4
- dataface/core/defaults/themes/stark.yaml +16 -7
- dataface/core/defaults/themes/vivid.yaml +15 -8
- dataface/core/diagnostics/__init__.py +2 -2
- dataface/core/diagnostics/codes_compile.py +13 -31
- dataface/core/diagnostics/codes_execute.py +79 -0
- dataface/core/diagnostics/codes_render.py +87 -28
- dataface/core/diagnostics/diagnostic.py +30 -0
- dataface/core/diagnostics/hints.py +19 -0
- dataface/core/dialects/base.py +17 -0
- dataface/core/dialects/postgres.py +14 -0
- dataface/core/dialects/redshift.py +5 -0
- dataface/core/execute/__init__.py +59 -14
- dataface/core/execute/adapters/dbt_adapter.py +4 -1
- dataface/core/execute/adapters/dbt_adapter_factory.py +19 -0
- dataface/core/execute/adapters/dbt_utils.py +84 -20
- dataface/core/execute/adapters/postgres_connection_manager.py +48 -0
- dataface/core/execute/adapters/sql_adapter.py +72 -4
- dataface/core/execute/dbt_jinja.py +65 -10
- dataface/core/execute/executor.py +69 -16
- dataface/core/font_measure.py +28 -0
- dataface/core/fonts.py +8 -5
- dataface/core/inspect/__init__.py +46 -6
- dataface/core/project.py +13 -1
- dataface/core/render/chart/data_table_attachment.py +14 -12
- dataface/core/render/chart/emitters/_cartesian.py +21 -18
- dataface/core/render/chart/emitters/_channels.py +56 -8
- dataface/core/render/chart/emitters/_label_overlap.py +18 -15
- dataface/core/render/chart/emitters/_measured_label_padding.py +4 -4
- dataface/core/render/chart/emitters/area.py +3 -3
- dataface/core/render/chart/emitters/bar.py +60 -35
- dataface/core/render/chart/emitters/geo.py +8 -13
- dataface/core/render/chart/emitters/heatmap.py +20 -26
- dataface/core/render/chart/emitters/line.py +2 -2
- dataface/core/render/chart/emitters/scatter.py +7 -30
- dataface/core/render/chart/features/baseline.py +22 -9
- dataface/core/render/chart/features/endpoint_labels.py +55 -32
- dataface/core/render/chart/features/mirror_axis.py +12 -9
- dataface/core/render/chart/features/value_labels.py +1 -1
- dataface/core/render/chart/kpi.py +11 -90
- dataface/core/render/chart/presentation.py +6 -8
- dataface/core/render/chart/rendering.py +3 -12
- dataface/core/render/chart/session.py +10 -0
- dataface/core/render/chart/spec.py +3 -0
- dataface/core/render/chart/spec_builders.py +18 -18
- dataface/core/render/chart/table.py +81 -36
- dataface/core/render/chart/table_support.py +2 -53
- dataface/core/render/chart/time_unit_detect.py +3 -2
- dataface/core/render/chart/translate.py +82 -44
- dataface/core/render/chart/type_inference.py +45 -25
- dataface/core/render/chart/validation.py +91 -0
- dataface/core/render/chart/vega_lite.py +30 -11
- dataface/core/render/chart/vl_field_maps.py +111 -76
- dataface/core/render/converters/chart.py +16 -6
- dataface/core/render/faces.py +3 -2
- dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.woff2 +0 -0
- dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.woff2 +0 -0
- dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -93
- dataface/core/render/fonts/_font_face.css +10 -0
- dataface/core/render/layout_sizing.py +267 -238
- dataface/core/render/numeral_expr.py +80 -0
- dataface/core/render/warnings/bar_band_width_too_narrow.py +64 -6
- dataface/core/render/warnings/layered_chart_shared_y_axis_scale_mismatch.py +25 -3
- dataface/core/render/warnings/likely_currency_or_percent_missing_formatter.py +12 -6
- dataface/core/render/warnings/palette_unsupported.py +6 -0
- dataface/core/render/warnings/pie_dominant_segment.py +1 -0
- dataface/core/render/warnings/pie_too_many_segments.py +1 -0
- dataface/core/render/warnings/point_map_out_of_projection.py +1 -0
- dataface/core/render/warnings/query_returned_zero_rows.py +1 -0
- dataface/core/render/warnings/redundant_encoding.py +20 -1
- dataface/core/render/warnings/registry.py +7 -3
- dataface/core/render/warnings/temporal_single_point.py +1 -0
- dataface/core/render/warnings/too_many_color_categories.py +1 -0
- dataface/core/render/warnings/too_many_x_categories.py +18 -2
- dataface/core/render/warnings/value_labels_crowd_width.py +9 -0
- dataface/core/render/warnings/y_encoding_mostly_null.py +1 -0
- dataface/core/render_format.py +18 -0
- dataface/core/text/format_d3.py +56 -0
- dataface/core/text/numeral_scale.py +218 -0
- dataface/core/utils.py +103 -10
- dataface/data/schemas/board-resolved/board-resolved.schema.json +9487 -7978
- dataface/data/schemas/yaml/0.3.2.json +1 -0
- dataface/data/schemas/yaml/manifest.json +7 -0
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/METADATA +1 -1
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/RECORD +185 -177
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/WHEEL +0 -0
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/entry_points.txt +0 -0
- {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/licenses/LICENSE +0 -0
|
@@ -10,7 +10,10 @@ from dataface.core.compile.models.chart.resolved.bar import ResolvedBarChart
|
|
|
10
10
|
from dataface.core.compile.models.primitives import OverlapSpec
|
|
11
11
|
from dataface.core.compile.tick_values import numeric_domain_bounds
|
|
12
12
|
from dataface.core.diagnostics.chart_data import ChartDataError
|
|
13
|
-
from dataface.core.diagnostics.codes_render import
|
|
13
|
+
from dataface.core.diagnostics.codes_render import (
|
|
14
|
+
ERR_HISTOGRAM_NON_NUMERIC,
|
|
15
|
+
ERR_LABEL_VALUES_NOT_TEMPORAL,
|
|
16
|
+
)
|
|
14
17
|
from dataface.core.render.chart._types import VLDict
|
|
15
18
|
from dataface.core.render.chart.emitters._cartesian import (
|
|
16
19
|
apply_domain_headroom_bounds,
|
|
@@ -51,14 +54,14 @@ from dataface.core.render.chart.type_inference import (
|
|
|
51
54
|
from dataface.core.render.chart.validation import (
|
|
52
55
|
validate_color_series,
|
|
53
56
|
validate_preaggregated_data,
|
|
57
|
+
validate_raw_rows_for_histogram,
|
|
54
58
|
)
|
|
55
59
|
from dataface.core.render.chart.vl_field_maps import (
|
|
56
|
-
SCALE_FIELD_MAP,
|
|
57
60
|
axis_to_vl,
|
|
58
61
|
bar_corner_props,
|
|
59
62
|
bar_mark_radius,
|
|
60
63
|
bar_mark_to_vl,
|
|
61
|
-
|
|
64
|
+
emit_resolved_scale_vl,
|
|
62
65
|
measure_axis_to_vl,
|
|
63
66
|
)
|
|
64
67
|
from dataface.core.render.utils import normalize_data_types
|
|
@@ -230,7 +233,9 @@ def _emit_histogram(
|
|
|
230
233
|
"""Emit a histogram spec: binned quantitative x, aggregate-count y, color-only grouping.
|
|
231
234
|
|
|
232
235
|
Mirrors V1 _map_histogram exactly. Does NOT call validate_preaggregated_data —
|
|
233
|
-
histograms consume raw rows (VL handles binning + aggregation)
|
|
236
|
+
histograms consume raw rows (VL handles binning + aggregation) — but does
|
|
237
|
+
call validate_raw_rows_for_histogram, the mirror-image guard that rejects
|
|
238
|
+
the opposite mistake: data that's already one row per bucket.
|
|
234
239
|
"""
|
|
235
240
|
# Validate before building: missing or non-numeric x crashes vl_convert.
|
|
236
241
|
if chart.x is None:
|
|
@@ -248,6 +253,7 @@ def _emit_histogram(
|
|
|
248
253
|
field=chart.x,
|
|
249
254
|
vl_type=x_vl_type,
|
|
250
255
|
)
|
|
256
|
+
validate_raw_rows_for_histogram(chart, data)
|
|
251
257
|
|
|
252
258
|
# Histogram bars are always positive (counts) → top-anchored corner rounding.
|
|
253
259
|
# bar_mark_to_vl no longer carries the corner radius (sign-aware rounding
|
|
@@ -414,7 +420,7 @@ class BarEmitter:
|
|
|
414
420
|
chart_id=chart.id,
|
|
415
421
|
axis_y=chart.style.axis_y,
|
|
416
422
|
tooltip_format=chart.style.tooltip_format,
|
|
417
|
-
background=chart.
|
|
423
|
+
background=chart.background,
|
|
418
424
|
single_series_fill=chart.style.single_series_fill,
|
|
419
425
|
legend=chart.legend,
|
|
420
426
|
config=config,
|
|
@@ -562,11 +568,7 @@ def _emit_vertical(
|
|
|
562
568
|
x_scale: dict[str, Any] = {}
|
|
563
569
|
if ax.scale is not None and ax.scale.padding is not None:
|
|
564
570
|
x_scale["padding"] = ax.scale.padding
|
|
565
|
-
|
|
566
|
-
if ax.scale is not None and ax.scale.band_padding_inner is not None:
|
|
567
|
-
x_scale["paddingInner"] = ax.scale.band_padding_inner
|
|
568
|
-
else:
|
|
569
|
-
x_scale["paddingInner"] = bar_mark.padding
|
|
571
|
+
x_scale["paddingInner"] = bar_mark.padding
|
|
570
572
|
x_scale.update(cartesian_x_scale_domain(ax.scale, x_vl_type, chart.id))
|
|
571
573
|
if x_vl_type == "quantitative" and bar_mark.size is not None:
|
|
572
574
|
# Vega-Lite centers each fixed continuousBandSize bar on its data
|
|
@@ -630,10 +632,11 @@ def _emit_vertical(
|
|
|
630
632
|
ay_vl["values"] = list(_NORMALIZE_QUARTILE_TICKS)
|
|
631
633
|
else:
|
|
632
634
|
is_stacked = bool(measure_field) and chart.stack not in (None, "none")
|
|
633
|
-
|
|
635
|
+
_ay_cont = ay.scale.continuous if ay.scale is not None else None
|
|
636
|
+
ay_scale_zero = _ay_cont.zero if _ay_cont is not None else None
|
|
634
637
|
bar_zero = not is_stacked and ay_scale_zero is not False
|
|
635
638
|
authored_domain = numeric_domain_bounds(
|
|
636
|
-
|
|
639
|
+
_ay_cont.domain if _ay_cont is not None else None
|
|
637
640
|
)
|
|
638
641
|
y_ticks: list[float] | None = list(ay.tick_values) if ay.tick_values else None
|
|
639
642
|
if y_ticks and "values" not in ay_vl:
|
|
@@ -642,14 +645,14 @@ def _emit_vertical(
|
|
|
642
645
|
if authored_domain is not None:
|
|
643
646
|
y_scale = {"domain": list(authored_domain)}
|
|
644
647
|
if ay.scale is not None:
|
|
645
|
-
y_scale.update(
|
|
648
|
+
y_scale.update(emit_resolved_scale_vl(ay.scale))
|
|
646
649
|
# An authored domain already IS the exact bounds — "zero" is a
|
|
647
650
|
# domain-inference hint, meaningless once the domain is pinned.
|
|
648
651
|
y_scale.pop("zero", None)
|
|
649
652
|
elif bar_zero:
|
|
650
653
|
y_scale = {"domainMin": (y_ticks[0] if y_ticks else 0.0), "zero": True}
|
|
651
654
|
if ay.scale is not None:
|
|
652
|
-
extra_scale =
|
|
655
|
+
extra_scale = emit_resolved_scale_vl(ay.scale)
|
|
653
656
|
extra_scale.pop("zero", None)
|
|
654
657
|
y_scale.update(extra_scale)
|
|
655
658
|
else:
|
|
@@ -780,6 +783,29 @@ def _emit_horizontal(
|
|
|
780
783
|
"chart must go through the resolve pipeline before emitting."
|
|
781
784
|
)
|
|
782
785
|
|
|
786
|
+
# style.axis_x.labels.values only has meaning against a temporal/ISO-bucketed
|
|
787
|
+
# scale (the labelExpr membership filter build_cartesian_x_encoding wires
|
|
788
|
+
# in for the vertical path). Horizontal bar's categorical axis is always
|
|
789
|
+
# emitted as a flat VL "nominal" y-encoding (see y_enc below) — there is no
|
|
790
|
+
# date-aware label path here for the filter to hook into, so honoring it
|
|
791
|
+
# would be a no-op regardless of whether the values are ISO-shaped. Raise
|
|
792
|
+
# the same coded error the vertical path raises for non-temporal x, rather
|
|
793
|
+
# than silently dropping the authored filter. Unlike the vertical path's
|
|
794
|
+
# guard, this fires unconditionally on ISO-ness (an explicit
|
|
795
|
+
# orientation: horizontal override with genuinely ISO-shaped x data would
|
|
796
|
+
# still be unable to honor labels.values), so the cause/remedy passed here
|
|
797
|
+
# name the orientation, never claim the data itself is invalid.
|
|
798
|
+
if ax.labels.values is not None:
|
|
799
|
+
raise ChartDataError.from_code(
|
|
800
|
+
ERR_LABEL_VALUES_NOT_TEMPORAL,
|
|
801
|
+
field=cat_field,
|
|
802
|
+
cause=(
|
|
803
|
+
"a horizontal bar's categorical axis never honors it, "
|
|
804
|
+
"regardless of date format"
|
|
805
|
+
),
|
|
806
|
+
remedy="Set orientation: vertical, or remove labels.values.",
|
|
807
|
+
)
|
|
808
|
+
|
|
783
809
|
# VL x = measure axis.
|
|
784
810
|
color_ch_early = chart.resolved_channels.get("color") if wide is False else None
|
|
785
811
|
measure_field = (
|
|
@@ -809,16 +835,15 @@ def _emit_horizontal(
|
|
|
809
835
|
# In Vega-Lite, paddingInner on a quantitative scale is a no-op visually, but
|
|
810
836
|
# V1 emits it unconditionally via effective.scale applied to all channels,
|
|
811
837
|
# and the existing tests assert it is present here.
|
|
812
|
-
|
|
838
|
+
_ay_cont_h = ay.scale.continuous if ay.scale is not None else None
|
|
839
|
+
_ay_scale_zero_h = _ay_cont_h.zero if _ay_cont_h is not None else None
|
|
813
840
|
x_enc_scale: VLDict = {
|
|
814
|
-
"zero":
|
|
841
|
+
"zero": _ay_scale_zero_h if isinstance(_ay_scale_zero_h, bool) else True
|
|
815
842
|
}
|
|
816
843
|
if ay.scale is not None:
|
|
817
|
-
extra_x_scale =
|
|
844
|
+
extra_x_scale = emit_resolved_scale_vl(ay.scale)
|
|
818
845
|
extra_x_scale.pop("zero", None)
|
|
819
846
|
x_enc_scale.update(extra_x_scale)
|
|
820
|
-
if ax.scale is not None and ax.scale.band_padding_inner is not None:
|
|
821
|
-
x_enc_scale["paddingInner"] = ax.scale.band_padding_inner
|
|
822
847
|
x_enc: dict[str, Any] = {
|
|
823
848
|
"field": measure_field,
|
|
824
849
|
"type": "quantitative",
|
|
@@ -835,8 +860,9 @@ def _emit_horizontal(
|
|
|
835
860
|
ay_vl["values"] = list(_NORMALIZE_QUARTILE_TICKS)
|
|
836
861
|
else:
|
|
837
862
|
# domainMax: use baked stacked total; fall back to nice-tick top.
|
|
863
|
+
_ay_cont_x = ay.scale.continuous if ay.scale is not None else None
|
|
838
864
|
authored_x_domain = numeric_domain_bounds(
|
|
839
|
-
|
|
865
|
+
_ay_cont_x.domain if _ay_cont_x is not None else None
|
|
840
866
|
)
|
|
841
867
|
if measure_field and chart.stack not in (None, "none"):
|
|
842
868
|
if authored_x_domain is None:
|
|
@@ -852,7 +878,10 @@ def _emit_horizontal(
|
|
|
852
878
|
x_enc["scale"], ay.domain_max, ay.domain_min
|
|
853
879
|
)
|
|
854
880
|
|
|
855
|
-
# Categorical axis (VL y): axis_to_vl(ax) + orient from ay.
|
|
881
|
+
# Categorical axis (VL y): axis_to_vl(ax) + orient from ay.position
|
|
882
|
+
# (categorical_orient was deleted 2026-08 — position now serves this role
|
|
883
|
+
# too; _resolve.py bakes it "left"-default here via _bake_ay_position_left,
|
|
884
|
+
# matching the deleted field's static default, before computing x_edge).
|
|
856
885
|
# Horizontal bar: categorical labels on VL y; is_horizontal_bar pins allow+angle=0.
|
|
857
886
|
label_layout = resolve_axis_x_overlap(
|
|
858
887
|
ax,
|
|
@@ -868,11 +897,11 @@ def _emit_horizontal(
|
|
|
868
897
|
label_overlap=label_layout.label_overlap,
|
|
869
898
|
label_angle=label_layout.angle,
|
|
870
899
|
)
|
|
871
|
-
if ay.
|
|
872
|
-
ax_vl["orient"] = ay.
|
|
873
|
-
# Own-side align (ax.
|
|
900
|
+
if ay.position is not None:
|
|
901
|
+
ax_vl["orient"] = ay.position
|
|
902
|
+
# Own-side align (ax.labels.align resolved to the axis's own
|
|
874
903
|
# orient side — either authored directly, or resolve-mapped from
|
|
875
|
-
# charts.bar.axis_x.
|
|
904
|
+
# charts.bar.axis_x.labels.align: inward) flips label growth back toward
|
|
876
905
|
# the plot. Symmetric case to vl_field_maps.py's measure_axis_to_vl guard.
|
|
877
906
|
cat_orient = ax_vl.get("orient")
|
|
878
907
|
# Every row, not a capped sample: an under-measured gutter from a wide
|
|
@@ -891,7 +920,7 @@ def _emit_horizontal(
|
|
|
891
920
|
# measure (zero rows). No font.case exclusion here: horizontal bar's
|
|
892
921
|
# categorical axis never runs inject_axis_label_case (that only wires
|
|
893
922
|
# into build_cartesian_x_encoding's temporal/ordinal x path, which this
|
|
894
|
-
# VL-y categorical axis doesn't go through), so
|
|
923
|
+
# VL-y categorical axis doesn't go through), so labels.font.case has no
|
|
895
924
|
# rendered effect to mismeasure against.
|
|
896
925
|
if (
|
|
897
926
|
cat_orient in ("left", "right")
|
|
@@ -900,12 +929,12 @@ def _emit_horizontal(
|
|
|
900
929
|
and cat_labels
|
|
901
930
|
):
|
|
902
931
|
cat_tick_size = (
|
|
903
|
-
ax.ticks.
|
|
932
|
+
ax.ticks.length if ax.ticks.length is not None else DEFAULT_VL_TICK_SIZE
|
|
904
933
|
)
|
|
905
|
-
cat_padding = measured_label_padding(cat_labels, ax.
|
|
934
|
+
cat_padding = measured_label_padding(cat_labels, ax.labels.font, cat_tick_size)
|
|
906
935
|
cat_label_limit = (
|
|
907
|
-
ax.
|
|
908
|
-
if ax.
|
|
936
|
+
ax.labels.max_width
|
|
937
|
+
if ax.labels.max_width is not None
|
|
909
938
|
else DEFAULT_VL_LABEL_LIMIT
|
|
910
939
|
)
|
|
911
940
|
ax_vl["labelPadding"] = cap_padding_to_label_limit(
|
|
@@ -935,11 +964,7 @@ def _emit_horizontal(
|
|
|
935
964
|
y_scale: dict[str, Any] = {}
|
|
936
965
|
if ax.scale is not None and ax.scale.padding is not None:
|
|
937
966
|
y_scale["padding"] = ax.scale.padding
|
|
938
|
-
|
|
939
|
-
if ax.scale is not None and ax.scale.band_padding_inner is not None:
|
|
940
|
-
y_scale["paddingInner"] = ax.scale.band_padding_inner
|
|
941
|
-
else:
|
|
942
|
-
y_scale["paddingInner"] = bar_mark.padding
|
|
967
|
+
y_scale["paddingInner"] = bar_mark.padding
|
|
943
968
|
|
|
944
969
|
y_enc: dict[str, Any] = {
|
|
945
970
|
"field": cat_field,
|
|
@@ -11,7 +11,11 @@ from dataface.core.compile.models.style.theme import (
|
|
|
11
11
|
GeoshapeChartStyle,
|
|
12
12
|
GeoshapeMarkStyle,
|
|
13
13
|
)
|
|
14
|
-
from dataface.core.render.chart.
|
|
14
|
+
from dataface.core.render.chart._types import VLDict
|
|
15
|
+
from dataface.core.render.chart.emitters._channels import (
|
|
16
|
+
channel_to_encoding,
|
|
17
|
+
gradient_scale_to_vl,
|
|
18
|
+
)
|
|
15
19
|
from dataface.core.render.chart.geo_tooltip import (
|
|
16
20
|
build_point_tooltip_fields,
|
|
17
21
|
validate_map_lookup_key_contract,
|
|
@@ -218,18 +222,9 @@ class GeoshapeEmitter:
|
|
|
218
222
|
if map_config is not None and map_config.color is not None
|
|
219
223
|
else None
|
|
220
224
|
)
|
|
221
|
-
_geo_scale:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
_geo_scale = {"scheme": _geo_gradient.palette}
|
|
225
|
-
else:
|
|
226
|
-
_geo_scale = {"range": list(_geo_gradient.palette)}
|
|
227
|
-
if _geo_gradient.min is not None and _geo_gradient.max is not None:
|
|
228
|
-
_geo_scale["domain"] = [_geo_gradient.min, _geo_gradient.max]
|
|
229
|
-
elif _geo_gradient.min is not None:
|
|
230
|
-
_geo_scale["domainMin"] = _geo_gradient.min
|
|
231
|
-
elif _geo_gradient.max is not None:
|
|
232
|
-
_geo_scale["domainMax"] = _geo_gradient.max
|
|
225
|
+
_geo_scale: VLDict = (
|
|
226
|
+
gradient_scale_to_vl(_geo_gradient) if _geo_gradient is not None else {}
|
|
227
|
+
)
|
|
233
228
|
color_enc = {
|
|
234
229
|
"field": value_field,
|
|
235
230
|
"type": "quantitative",
|
|
@@ -6,7 +6,11 @@ from dataclasses import dataclass
|
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
8
|
from dataface.core.compile.models.chart.resolved.heatmap import ResolvedHeatmapChart
|
|
9
|
-
from dataface.core.render.chart.emitters._channels import
|
|
9
|
+
from dataface.core.render.chart.emitters._channels import (
|
|
10
|
+
apply_color_legend,
|
|
11
|
+
channel_to_encoding,
|
|
12
|
+
gradient_scale_to_vl,
|
|
13
|
+
)
|
|
10
14
|
from dataface.core.render.chart.emitters._label_overlap import resolve_axis_x_overlap
|
|
11
15
|
from dataface.core.render.chart.spec import ChartSpec, RenderBox
|
|
12
16
|
from dataface.core.render.chart.vl_field_maps import (
|
|
@@ -114,7 +118,7 @@ class HeatmapEmitter:
|
|
|
114
118
|
y_type = "nominal"
|
|
115
119
|
# Row labels are the literal per-row values (no d3 format applies
|
|
116
120
|
# to a nominal field), so an own-side align (resolved from
|
|
117
|
-
# axis_y.
|
|
121
|
+
# axis_y.labels.align: inward, or an authored left/right) is
|
|
118
122
|
# measured directly against them — same exact-content case
|
|
119
123
|
# bar.py's categorical own-side gate uses. Only worth the
|
|
120
124
|
# O(rows) dedup pass when align is actually set — otherwise
|
|
@@ -128,7 +132,7 @@ class HeatmapEmitter:
|
|
|
128
132
|
if row.get(chart.y) is not None
|
|
129
133
|
)
|
|
130
134
|
)
|
|
131
|
-
if ay.
|
|
135
|
+
if ay.labels.align is not None
|
|
132
136
|
else ()
|
|
133
137
|
)
|
|
134
138
|
y_axis = measure_axis_to_vl(ay, data, (), category_labels=y_category_labels)
|
|
@@ -157,31 +161,21 @@ class HeatmapEmitter:
|
|
|
157
161
|
from_slug=True,
|
|
158
162
|
font=chart.legend.title.font,
|
|
159
163
|
)
|
|
160
|
-
#
|
|
161
|
-
#
|
|
162
|
-
|
|
163
|
-
#
|
|
164
|
+
# Heatmap's color channel is the whole measure, so it needs a key
|
|
165
|
+
# like every other family — wire the resolved legend style through
|
|
166
|
+
# like bar/line/scatter/pie do. VL renders its native continuous
|
|
167
|
+
# gradient legend for a quantitative field with no extra code.
|
|
168
|
+
apply_color_legend(enc, chart.legend)
|
|
169
|
+
# color_gradient is the cascade-complete gradient (theme/board
|
|
170
|
+
# defaults merged with any chart-local style.color.gradient
|
|
171
|
+
# override) — strictly more complete than a "gradient" mode
|
|
172
|
+
# channel's own ch.scale, which is built from the chart-local
|
|
173
|
+
# gradient dict alone and can be missing a domain the cascade
|
|
174
|
+
# supplies (e.g. board-level min/max). Always prefer it over
|
|
175
|
+
# whatever channel_to_encoding already set.
|
|
164
176
|
_cg = chart.style.color_gradient
|
|
165
177
|
if _cg is not None:
|
|
166
|
-
|
|
167
|
-
_range = (
|
|
168
|
-
list(_cg.palette) if isinstance(_cg.palette, list) else None
|
|
169
|
-
)
|
|
170
|
-
if "scale" not in enc:
|
|
171
|
-
enc["scale"] = {}
|
|
172
|
-
if _scheme is not None and "range" not in enc["scale"]:
|
|
173
|
-
enc["scale"] = {**enc["scale"], "scheme": _scheme}
|
|
174
|
-
elif _range is not None and "range" not in enc["scale"]:
|
|
175
|
-
enc["scale"] = {**enc["scale"], "range": _range}
|
|
176
|
-
if _cg.min is not None and _cg.max is not None:
|
|
177
|
-
enc["scale"] = {
|
|
178
|
-
**enc["scale"],
|
|
179
|
-
"domain": [_cg.min, _cg.max],
|
|
180
|
-
}
|
|
181
|
-
elif _cg.min is not None:
|
|
182
|
-
enc["scale"] = {**enc["scale"], "domainMin": _cg.min}
|
|
183
|
-
elif _cg.max is not None:
|
|
184
|
-
enc["scale"] = {**enc["scale"], "domainMax": _cg.max}
|
|
178
|
+
enc["scale"] = gradient_scale_to_vl(_cg)
|
|
185
179
|
# Tooltip format on color encoding for quantitative heatmap values.
|
|
186
180
|
if enc.get("type") == "quantitative":
|
|
187
181
|
fmt = chart.style.tooltip_format
|
|
@@ -381,7 +381,7 @@ class LineEmitter:
|
|
|
381
381
|
sub_layers = emit_line_layer(
|
|
382
382
|
style.line_mark,
|
|
383
383
|
style.point_mark,
|
|
384
|
-
|
|
384
|
+
chart.background,
|
|
385
385
|
style.single_series_fill,
|
|
386
386
|
has_color_enc,
|
|
387
387
|
top_encoding["color"] if has_color_enc else {},
|
|
@@ -416,7 +416,7 @@ class LineEmitter:
|
|
|
416
416
|
chart_id=chart.id,
|
|
417
417
|
axis_y=style.axis_y,
|
|
418
418
|
tooltip_format=style.tooltip_format,
|
|
419
|
-
background=
|
|
419
|
+
background=chart.background,
|
|
420
420
|
single_series_fill=style.single_series_fill,
|
|
421
421
|
legend=chart.legend,
|
|
422
422
|
config=build_palette_config(chart.palette),
|
|
@@ -7,7 +7,6 @@ from decimal import Decimal
|
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
9
|
from dataface.core.compile.models.chart.resolved.scatter import ResolvedScatterChart
|
|
10
|
-
from dataface.core.compile.models.style.theme import ScaleStyle
|
|
11
10
|
from dataface.core.render.chart.emitters._cartesian import (
|
|
12
11
|
apply_domain_headroom_bounds,
|
|
13
12
|
build_palette_config,
|
|
@@ -23,24 +22,14 @@ from dataface.core.render.chart.emitters._layers import emit_scatter_layer
|
|
|
23
22
|
from dataface.core.render.chart.spec import ChartSpec, RenderBox
|
|
24
23
|
from dataface.core.render.chart.type_inference import _utc_time_label_expr
|
|
25
24
|
from dataface.core.render.chart.vl_field_maps import (
|
|
26
|
-
SCALE_ENCODING_FIELD_MAP,
|
|
27
|
-
SCALE_FIELD_MAP,
|
|
28
25
|
axis_to_vl,
|
|
29
|
-
|
|
26
|
+
emit_resolved_scale_vl,
|
|
30
27
|
measure_axis_to_vl,
|
|
31
28
|
)
|
|
32
29
|
from dataface.core.text.case import format_display_text
|
|
33
30
|
from dataface.core.text.format_d3 import is_time_format
|
|
34
31
|
|
|
35
32
|
|
|
36
|
-
def _build_axis_scale(scale: ScaleStyle) -> dict[str, Any]:
|
|
37
|
-
"""Build the VL scale dict from board-level scale only."""
|
|
38
|
-
out: dict[str, Any] = {}
|
|
39
|
-
out.update(map_fields(scale, SCALE_FIELD_MAP))
|
|
40
|
-
out.update(map_fields(scale, SCALE_ENCODING_FIELD_MAP))
|
|
41
|
-
return out
|
|
42
|
-
|
|
43
|
-
|
|
44
33
|
def _suppress_zero_for_scatter_y(data: list[dict[str, Any]], y_field: str) -> bool:
|
|
45
34
|
"""Return True when scatter y-axis zero should be suppressed (ratio > 0.25).
|
|
46
35
|
|
|
@@ -82,16 +71,10 @@ class ScatterEmitter:
|
|
|
82
71
|
# Scatter x is always quantitative (continuous). The strategy resolver
|
|
83
72
|
# short-circuits to allow+angle=0 for continuous x; mirror that here
|
|
84
73
|
# by pinning labelAngle explicitly so VL doesn't auto-rotate.
|
|
85
|
-
if ax.
|
|
74
|
+
if ax.labels.angle is None:
|
|
86
75
|
x_axis["labelAngle"] = 0.0
|
|
87
|
-
x_scale = (
|
|
88
|
-
|
|
89
|
-
if chart.style.scale is not None
|
|
90
|
-
else {}
|
|
91
|
-
)
|
|
92
|
-
if ax.scale is not None:
|
|
93
|
-
x_scale.update(map_fields(ax.scale, SCALE_FIELD_MAP))
|
|
94
|
-
x_scale.update(map_fields(ax.scale, SCALE_ENCODING_FIELD_MAP))
|
|
76
|
+
x_scale = emit_resolved_scale_vl(chart.style.scale, include_x_only=True)
|
|
77
|
+
x_scale.update(emit_resolved_scale_vl(ax.scale, include_x_only=True))
|
|
95
78
|
# Use axis title font to match oracle (effective.axis_x.title.font).
|
|
96
79
|
x_enc: dict[str, Any] = {
|
|
97
80
|
"field": chart.x,
|
|
@@ -130,14 +113,8 @@ class ScatterEmitter:
|
|
|
130
113
|
if ay.tick_values:
|
|
131
114
|
y_axis["values"] = list(ay.tick_values)
|
|
132
115
|
|
|
133
|
-
y_scale = (
|
|
134
|
-
|
|
135
|
-
if chart.style.scale is not None
|
|
136
|
-
else {}
|
|
137
|
-
)
|
|
138
|
-
if ay.scale is not None:
|
|
139
|
-
y_scale.update(map_fields(ay.scale, SCALE_FIELD_MAP))
|
|
140
|
-
y_scale.update(map_fields(ay.scale, SCALE_ENCODING_FIELD_MAP))
|
|
116
|
+
y_scale = emit_resolved_scale_vl(chart.style.scale)
|
|
117
|
+
y_scale.update(emit_resolved_scale_vl(ay.scale))
|
|
141
118
|
if y_type == "quantitative" and "domain" not in y_scale:
|
|
142
119
|
# Exact headroom-applied bounds baked at resolve(); an authored
|
|
143
120
|
# `domain` (mapped above) always wins outright.
|
|
@@ -234,7 +211,7 @@ class ScatterEmitter:
|
|
|
234
211
|
chart_id=chart.id,
|
|
235
212
|
axis_y=chart.style.axis_y,
|
|
236
213
|
tooltip_format=chart.style.tooltip_format,
|
|
237
|
-
background=chart.
|
|
214
|
+
background=chart.background,
|
|
238
215
|
single_series_fill=chart.style.single_series_fill,
|
|
239
216
|
legend=chart.legend,
|
|
240
217
|
config=config,
|
|
@@ -165,7 +165,8 @@ def _domain_reaches(
|
|
|
165
165
|
percent-format chart whose data stays well below 1.0.
|
|
166
166
|
"""
|
|
167
167
|
scale = chart.style.axis_y.scale # type: ignore[union-attr]
|
|
168
|
-
|
|
168
|
+
_sc_cont_bv = scale.continuous if scale is not None else None
|
|
169
|
+
domain = _sc_cont_bv.domain if _sc_cont_bv is not None else None
|
|
169
170
|
if (
|
|
170
171
|
domain is not None
|
|
171
172
|
and len(domain) >= 2
|
|
@@ -261,15 +262,17 @@ class BaselineFeature:
|
|
|
261
262
|
# log domain cannot represent — Vega-Lite's entire axis rendering
|
|
262
263
|
# breaks (empirically: every tick label vanishes), not just this layer.
|
|
263
264
|
axis_y_scale = chart.style.axis_y.scale
|
|
264
|
-
if axis_y_scale is not None
|
|
265
|
+
_bsl_cont = axis_y_scale.continuous if axis_y_scale is not None else None
|
|
266
|
+
if _bsl_cont is not None and _bsl_cont.type == "log":
|
|
265
267
|
return
|
|
266
268
|
# Determine whether the rule should fire.
|
|
267
269
|
if isinstance(chart, ResolvedBarChart):
|
|
268
270
|
should_fire = True
|
|
269
271
|
# An explicit measure-axis domain that excludes 0 suppresses the rule.
|
|
270
272
|
_scale = chart.style.axis_y.scale
|
|
271
|
-
if _scale is not None
|
|
272
|
-
|
|
273
|
+
_bsl_cont2 = _scale.continuous if _scale is not None else None
|
|
274
|
+
if _bsl_cont2 is not None and _bsl_cont2.domain is not None:
|
|
275
|
+
_d = _bsl_cont2.domain
|
|
273
276
|
if (
|
|
274
277
|
len(_d) >= 2
|
|
275
278
|
and all(isinstance(v, (int, float)) for v in _d)
|
|
@@ -285,7 +288,8 @@ class BaselineFeature:
|
|
|
285
288
|
if measure_field is False:
|
|
286
289
|
return
|
|
287
290
|
scale = chart.style.axis_y.scale
|
|
288
|
-
|
|
291
|
+
_bsl_cont3 = scale.continuous if scale is not None else None
|
|
292
|
+
zero_setting = _bsl_cont3.zero if _bsl_cont3 is not None else None
|
|
289
293
|
if zero_setting is False:
|
|
290
294
|
# The straddle check must cover every mark sharing this SAME
|
|
291
295
|
# y scale, not just the base series' own values — apply()
|
|
@@ -335,12 +339,14 @@ class BaselineFeature:
|
|
|
335
339
|
|
|
336
340
|
if measure_field is False:
|
|
337
341
|
return
|
|
338
|
-
|
|
339
342
|
# Read zero style from the chart's own baked axis cascade (style.axis_y),
|
|
340
343
|
# not board-level style — a chart-local axis patch has to win.
|
|
341
344
|
if not chart.style.axis_y.grid.visible:
|
|
342
345
|
return
|
|
343
346
|
zero_style = chart.style.axis_y.grid.zero
|
|
347
|
+
assert (
|
|
348
|
+
zero_style is not None
|
|
349
|
+
), "zero grid style must be resolved before emitting the baseline rule"
|
|
344
350
|
rule = _full_rule_at(
|
|
345
351
|
0,
|
|
346
352
|
axis=rule_axis,
|
|
@@ -358,7 +364,8 @@ class BaselineFeature:
|
|
|
358
364
|
assert isinstance(chart, (ResolvedBarChart, ResolvedAreaChart))
|
|
359
365
|
# Same log-domain incompatibility as _insert_zero_rule's datum:0 guard.
|
|
360
366
|
axis_y_scale = chart.style.axis_y.scale
|
|
361
|
-
if axis_y_scale is not None
|
|
367
|
+
_top_cont = axis_y_scale.continuous if axis_y_scale is not None else None
|
|
368
|
+
if _top_cont is not None and _top_cont.type == "log":
|
|
362
369
|
return
|
|
363
370
|
rule_axis = (
|
|
364
371
|
"x"
|
|
@@ -369,6 +376,9 @@ class BaselineFeature:
|
|
|
369
376
|
if measure_field is False:
|
|
370
377
|
return
|
|
371
378
|
zero_style = chart.style.axis_y.grid.zero
|
|
379
|
+
assert (
|
|
380
|
+
zero_style is not None
|
|
381
|
+
), "zero grid style must be resolved before emitting top rules"
|
|
372
382
|
# Horizontal bars put the measure on x, so the 0%/100% datum rules anchor
|
|
373
383
|
# on x — not the categorical y axis (mirrors the zero rule).
|
|
374
384
|
for datum in (0, 1):
|
|
@@ -392,10 +402,10 @@ class BaselineFeature:
|
|
|
392
402
|
) -> None:
|
|
393
403
|
if not isinstance(chart, (ResolvedLineChart, ResolvedAreaChart)):
|
|
394
404
|
return
|
|
395
|
-
# axis_y.format is the D3 spec resolved from the face+theme cascade.
|
|
405
|
+
# axis_y.labels.format is the D3 spec resolved from the face+theme cascade.
|
|
396
406
|
# Check it first — it converts Dataface aliases ("percent_whole") to
|
|
397
407
|
# their literal D3 form (".0%"), which always contains "%".
|
|
398
|
-
ax_fmt = chart.style.axis_y.format
|
|
408
|
+
ax_fmt = chart.style.axis_y.labels.format
|
|
399
409
|
if not ((ax_fmt and "%" in ax_fmt) or _is_percent_format(chart.format)):
|
|
400
410
|
return
|
|
401
411
|
# grid.visible=False suppresses the unity rule (mirrors zero-rule gate).
|
|
@@ -418,6 +428,9 @@ class BaselineFeature:
|
|
|
418
428
|
):
|
|
419
429
|
return
|
|
420
430
|
zero_style = chart.style.axis_y.grid.zero
|
|
431
|
+
assert (
|
|
432
|
+
zero_style is not None
|
|
433
|
+
), "zero grid style must be resolved before emitting unity rule"
|
|
421
434
|
_insert_rule(
|
|
422
435
|
spec,
|
|
423
436
|
_full_rule_at(
|