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
|
@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any, TypeAlias
|
|
|
9
9
|
|
|
10
10
|
from dataface.core.compile.config import get_chart_rendering
|
|
11
11
|
from dataface.core.render.chart.artifacts import ChartRenderData
|
|
12
|
-
from dataface.core.render.chart.vl_field_maps import
|
|
12
|
+
from dataface.core.render.chart.vl_field_maps import emit_resolved_scale_vl
|
|
13
13
|
from dataface.core.text.format_d3 import is_time_format
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
|
|
|
17
17
|
|
|
18
18
|
# Date-only ISO forms (YYYY-MM, YYYY-MM-DD) — the only string shapes JS
|
|
19
19
|
# Date.parse treats as UTC midnight, making them safe domain values for the
|
|
20
|
-
#
|
|
20
|
+
# labels.values epoch-ms membership filter on ordinal axes. Datetime strings
|
|
21
21
|
# without an offset parse as LOCAL time in JS and are deliberately excluded.
|
|
22
22
|
_ISO_UTC_SAFE_RE = re.compile(r"^\d{4}-(0[1-9]|1[0-2])(-(0[1-9]|[12]\d|3[01]))?$")
|
|
23
23
|
|
|
@@ -126,7 +126,10 @@ def resolve_authored_x_type(axis: ResolvedAxisStyle) -> str | None:
|
|
|
126
126
|
"""
|
|
127
127
|
authored_type = axis.type
|
|
128
128
|
if authored_type in (None, "auto") and axis.scale is not None:
|
|
129
|
-
|
|
129
|
+
_ax_scale_type = (
|
|
130
|
+
axis.scale.continuous.type if axis.scale.continuous is not None else None
|
|
131
|
+
)
|
|
132
|
+
if _ax_scale_type == "temporal":
|
|
130
133
|
authored_type = "temporal"
|
|
131
134
|
return authored_type
|
|
132
135
|
|
|
@@ -196,7 +199,7 @@ def resolve_cartesian_x_type(
|
|
|
196
199
|
|
|
197
200
|
def temporal_edge_labels_flushed(vl_type: str, axis: ResolvedAxisStyle) -> bool:
|
|
198
201
|
"""Whether Vega flushes this temporal axis's outer labels into the plot."""
|
|
199
|
-
return vl_type == "temporal" and axis.
|
|
202
|
+
return vl_type == "temporal" and axis.labels.flush is not False
|
|
200
203
|
|
|
201
204
|
|
|
202
205
|
def build_cartesian_x_encoding(
|
|
@@ -250,7 +253,7 @@ def build_cartesian_x_encoding(
|
|
|
250
253
|
)
|
|
251
254
|
from dataface.core.render.chart.vl_field_maps import _n
|
|
252
255
|
|
|
253
|
-
label_values: list[Any] | None = _n(axis, "
|
|
256
|
+
label_values: list[Any] | None = _n(axis, "labels", "values")
|
|
254
257
|
|
|
255
258
|
# label_tu is the resolved label cadence for a bucketed grain — computed once
|
|
256
259
|
# so both the density gate below and the axis-values/labelExpr enrichment
|
|
@@ -258,7 +261,7 @@ def build_cartesian_x_encoding(
|
|
|
258
261
|
label_tu: str | None = None
|
|
259
262
|
authored_label_tu: str | None = None
|
|
260
263
|
if time_unit and time_unit in BUCKETED_CALENDAR_UNITS:
|
|
261
|
-
authored_label_tu = getattr(getattr(axis, "
|
|
264
|
+
authored_label_tu = getattr(getattr(axis, "labels", None), "time_unit", None)
|
|
262
265
|
label_tu = (
|
|
263
266
|
format_time_unit
|
|
264
267
|
if format_time_unit
|
|
@@ -289,7 +292,7 @@ def build_cartesian_x_encoding(
|
|
|
289
292
|
label_tu in BUCKETED_CALENDAR_UNITS
|
|
290
293
|
and label_tu != time_unit
|
|
291
294
|
and axis.ticks.count is None
|
|
292
|
-
and axis.ticks.
|
|
295
|
+
and axis.ticks.time_unit is None
|
|
293
296
|
and "values" not in ax_vl
|
|
294
297
|
)
|
|
295
298
|
label_tick_values: list[Any] = []
|
|
@@ -322,7 +325,7 @@ def build_cartesian_x_encoding(
|
|
|
322
325
|
)
|
|
323
326
|
temporal_anchor_value = str((anchor_openers or anchor_values[:1])[0])
|
|
324
327
|
|
|
325
|
-
#
|
|
328
|
+
# labels.values needs the epoch-ms membership filter to match every
|
|
326
329
|
# listed date exactly. On a continuous temporal scale datum.value is
|
|
327
330
|
# already epoch ms — always exact. On any other emit path datum.value is
|
|
328
331
|
# the raw domain value, which Vega parses with JS Date semantics: only
|
|
@@ -350,7 +353,11 @@ def build_cartesian_x_encoding(
|
|
|
350
353
|
raise ChartDataError.from_code(
|
|
351
354
|
ERR_LABEL_VALUES_NOT_TEMPORAL,
|
|
352
355
|
field=x_field,
|
|
353
|
-
|
|
356
|
+
cause=f"it contains {row[x_field]!r}, which isn't a UTC-safe ISO date",
|
|
357
|
+
remedy=(
|
|
358
|
+
"Use date-only ISO dates (YYYY-MM-DD or YYYY-MM) or date "
|
|
359
|
+
"objects in the query, or remove labels.values."
|
|
360
|
+
),
|
|
354
361
|
)
|
|
355
362
|
|
|
356
363
|
result = dict(ax_vl)
|
|
@@ -430,15 +437,15 @@ def build_cartesian_x_encoding(
|
|
|
430
437
|
)
|
|
431
438
|
if smart_expr_t is not None:
|
|
432
439
|
result["labelExpr"] = smart_expr_t
|
|
433
|
-
# Step-anchored cadence (axis.ticks.
|
|
440
|
+
# Step-anchored cadence (axis.ticks.time_unit/step) requires a genuinely
|
|
434
441
|
# continuous temporal x-axis — compile-time gates (_bake_cartesian_axes)
|
|
435
|
-
# already reject it on axis_y and reject step-without-
|
|
442
|
+
# already reject it on axis_y and reject step-without-time_unit, but
|
|
436
443
|
# whether THIS axis resolves to temporal vs ordinal is a data-dependent
|
|
437
444
|
# decision only known here. Unlike count (which silently no-ops on
|
|
438
|
-
# ordinal — see the density-gate test suite),
|
|
445
|
+
# ordinal — see the density-gate test suite), time_unit/step raise: the
|
|
439
446
|
# whole point of the surface is an explicit cadence, so a silent no-op
|
|
440
447
|
# would hide a misconfiguration rather than surface it.
|
|
441
|
-
if axis.ticks.
|
|
448
|
+
if axis.ticks.time_unit is not None and vl_type != "temporal":
|
|
442
449
|
from dataface.core.diagnostics.chart_data import ChartDataError
|
|
443
450
|
from dataface.core.diagnostics.codes_render import (
|
|
444
451
|
ERR_TICKS_INTERVAL_NOT_TEMPORAL,
|
|
@@ -448,17 +455,29 @@ def build_cartesian_x_encoding(
|
|
|
448
455
|
ERR_TICKS_INTERVAL_NOT_TEMPORAL, field=x_field, vl_type=vl_type
|
|
449
456
|
)
|
|
450
457
|
|
|
451
|
-
# Authored ticks.count/
|
|
458
|
+
# Authored ticks.count/time_unit on a temporal scale maps straight to VL's
|
|
452
459
|
# axis.tickCount (a plain number, or {interval, step} for step-anchored
|
|
453
|
-
# cadence —
|
|
460
|
+
# cadence — VL's own wire format uses "interval" regardless of Dataface's
|
|
461
|
+
# field name — see _resolve_cartesian_ticks for the quantitative-axis
|
|
454
462
|
# explicit-values path, which is unrelated). Ordinal axes are out of
|
|
455
463
|
# scope: tickCount has no meaning against a discrete-domain ordinal
|
|
456
464
|
# scale, so this never fires for vl_type == "ordinal". axis.ticks is a
|
|
457
465
|
# required ResolvedAxisStyle field — read it directly and crash loudly on
|
|
458
466
|
# a malformed axis object rather than silently skipping.
|
|
459
467
|
if vl_type == "temporal" and "tickCount" not in result and "values" not in result:
|
|
460
|
-
|
|
461
|
-
|
|
468
|
+
_ticks_time_unit = axis.ticks.time_unit
|
|
469
|
+
if _ticks_time_unit is not None:
|
|
470
|
+
if _ticks_time_unit not in _TEMPORAL_TICK_INTERVAL:
|
|
471
|
+
from dataface.core.diagnostics.chart_data import ChartDataError
|
|
472
|
+
|
|
473
|
+
raise ChartDataError(
|
|
474
|
+
f"ticks.time_unit: {_ticks_time_unit!r} has no step-anchored "
|
|
475
|
+
"cadence — only a calendar-bucketing grain "
|
|
476
|
+
f"({', '.join(_TEMPORAL_TICK_INTERVAL)}) names a VL tick "
|
|
477
|
+
"interval."
|
|
478
|
+
)
|
|
479
|
+
interval, _ = _TEMPORAL_TICK_INTERVAL[_ticks_time_unit]
|
|
480
|
+
tick_count: dict[str, Any] = {"interval": interval}
|
|
462
481
|
if axis.ticks.step is not None:
|
|
463
482
|
tick_count["step"] = axis.ticks.step
|
|
464
483
|
result["tickCount"] = tick_count
|
|
@@ -478,7 +497,7 @@ def build_cartesian_x_encoding(
|
|
|
478
497
|
)
|
|
479
498
|
|
|
480
499
|
result = inject_axis_label_case(result, axis)
|
|
481
|
-
#
|
|
500
|
+
# labels.values filter runs last of all — it wins over any smart-cadence
|
|
482
501
|
# or case-transformed text already resolved above.
|
|
483
502
|
result = inject_axis_label_values_filter(result, axis)
|
|
484
503
|
return vl_type, result, time_unit
|
|
@@ -490,21 +509,22 @@ def y_zero_scale(
|
|
|
490
509
|
) -> dict[str, Any]:
|
|
491
510
|
"""Build a VL y-scale dict from the resolved y-axis style.
|
|
492
511
|
|
|
493
|
-
Merges every
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
512
|
+
Merges every scale field (``type``, ``base``, ``exponent``, etc.) via
|
|
513
|
+
``emit_resolved_scale_vl`` — one canonical emitter, so a measure-axis
|
|
514
|
+
``scale.type: log`` (previously honored only on scatter) reaches
|
|
515
|
+
line/area/bar too. ``zero`` is excluded from that merge and computed
|
|
516
|
+
separately:
|
|
498
517
|
|
|
499
518
|
axis.scale.zero is True → {"domainMin": tick_values[0] or 0.0, "zero": True}
|
|
500
519
|
anything else → {"zero": False}
|
|
501
520
|
"""
|
|
502
521
|
scale = getattr(axis, "scale", None) if axis is not None else None
|
|
503
|
-
|
|
522
|
+
_sc_cont = scale.continuous if scale is not None else None
|
|
523
|
+
zero = _sc_cont.zero if _sc_cont is not None else None
|
|
504
524
|
|
|
505
525
|
out: dict[str, Any] = {}
|
|
506
526
|
if scale is not None:
|
|
507
|
-
out.update(
|
|
527
|
+
out.update(emit_resolved_scale_vl(scale))
|
|
508
528
|
out.pop("zero", None) # zero is computed below, not passed through raw
|
|
509
529
|
|
|
510
530
|
if zero is True:
|
|
@@ -6,6 +6,7 @@ Keeps data-shape policy separate from mechanical spec assembly.
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
from collections import Counter
|
|
9
|
+
from decimal import Decimal
|
|
9
10
|
from typing import Any
|
|
10
11
|
|
|
11
12
|
from dataface.core.compile.models.chart.resolved import (
|
|
@@ -18,9 +19,15 @@ from dataface.core.diagnostics.chart_data import ChartDataError
|
|
|
18
19
|
from dataface.core.diagnostics.codes_render import (
|
|
19
20
|
ERR_BAR_DUPLICATE_ROWS,
|
|
20
21
|
ERR_COLOR_NULL_SERIES,
|
|
22
|
+
ERR_HISTOGRAM_PREAGGREGATED,
|
|
21
23
|
)
|
|
22
24
|
from dataface.core.render.chart.type_inference import infer_vega_type_from_data
|
|
23
25
|
|
|
26
|
+
# A run this short forming a gapless integer sequence by chance is not
|
|
27
|
+
# meaningfully unusual for genuine raw data; below this length the "dense
|
|
28
|
+
# integer run" signal in _forms_gapless_integer_run is too weak to act on.
|
|
29
|
+
_MIN_RUN_FOR_GROUP_BY_SHAPE = 5
|
|
30
|
+
|
|
24
31
|
|
|
25
32
|
def validate_color_series(
|
|
26
33
|
chart: ResolvedBarChart | ResolvedLineChart | ResolvedAreaChart,
|
|
@@ -172,3 +179,87 @@ def validate_preaggregated_data(
|
|
|
172
179
|
field_list=field_list,
|
|
173
180
|
duplicate_preview=duplicate_preview,
|
|
174
181
|
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _forms_gapless_integer_run(values: list[Any]) -> bool:
|
|
185
|
+
"""True when every value is integer-valued, none repeats, and together
|
|
186
|
+
they cover every integer between the min and max with no gaps.
|
|
187
|
+
|
|
188
|
+
This is exactly the shape ``GROUP BY <integer column>`` produces: one row
|
|
189
|
+
per distinct observed value. It is deliberately *not* "zero duplicate
|
|
190
|
+
values" alone — a continuous measure (the primary histogram use case,
|
|
191
|
+
e.g. price, latency) is essentially always duplicate-free too, so that
|
|
192
|
+
signal can't tell raw from aggregated. A dense, gapless run of whole
|
|
193
|
+
numbers is different: real raw samples have gaps and skew; hitting every
|
|
194
|
+
integer in a range exactly once is the kind of thing an integer bucket
|
|
195
|
+
key produces by construction; a continuous measure practically never
|
|
196
|
+
produces by chance.
|
|
197
|
+
"""
|
|
198
|
+
if len(values) < _MIN_RUN_FOR_GROUP_BY_SHAPE:
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
whole_numbers: set[int] = set()
|
|
202
|
+
for value in values:
|
|
203
|
+
if isinstance(value, bool):
|
|
204
|
+
return False
|
|
205
|
+
if isinstance(value, int):
|
|
206
|
+
whole = value
|
|
207
|
+
elif (isinstance(value, float) and value.is_integer()) or (
|
|
208
|
+
isinstance(value, Decimal) and value == value.to_integral_value()
|
|
209
|
+
):
|
|
210
|
+
whole = int(value)
|
|
211
|
+
else:
|
|
212
|
+
return False
|
|
213
|
+
if whole in whole_numbers:
|
|
214
|
+
return False
|
|
215
|
+
whole_numbers.add(whole)
|
|
216
|
+
|
|
217
|
+
return max(whole_numbers) - min(whole_numbers) + 1 == len(whole_numbers)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def validate_raw_rows_for_histogram(
|
|
221
|
+
chart: ResolvedBarChart,
|
|
222
|
+
data: list[dict[str, Any]],
|
|
223
|
+
) -> None:
|
|
224
|
+
"""Reject pre-aggregated data on a histogram — the mirror image of
|
|
225
|
+
``validate_preaggregated_data``.
|
|
226
|
+
|
|
227
|
+
Histograms hand Vega-Lite raw, ungrouped rows and let VL do its own
|
|
228
|
+
binning + counting; a histogram's ``y`` is always ``aggregate: count``,
|
|
229
|
+
never a data column. A pre-aggregated table — the output of
|
|
230
|
+
``SELECT x, count(*) FROM t GROUP BY x`` — carries that count (and
|
|
231
|
+
possibly other aggregates) in leftover numeric columns the emitter never
|
|
232
|
+
reads, and ``x`` forms a gapless run of whole numbers (what ``GROUP BY``
|
|
233
|
+
over an integer bucket produces — see ``_forms_gapless_integer_run``).
|
|
234
|
+
Both conditions together are the tell; either alone is common in genuine
|
|
235
|
+
raw data (a raw fact table often carries a spare numeric column — an id,
|
|
236
|
+
a second measure — and a continuous measure is duplicate-free as a
|
|
237
|
+
matter of course). Feeding pre-aggregated data to VL would bin and
|
|
238
|
+
recount the already-counted rows, silently discarding the real count.
|
|
239
|
+
"""
|
|
240
|
+
if not data or chart.x is None:
|
|
241
|
+
return
|
|
242
|
+
x_field = chart.x
|
|
243
|
+
|
|
244
|
+
color_field = effective_color_field(chart)
|
|
245
|
+
extra_columns = [
|
|
246
|
+
column for column in data[0] if column not in {x_field, color_field}
|
|
247
|
+
]
|
|
248
|
+
numeric_extra_columns = [
|
|
249
|
+
column
|
|
250
|
+
for column in extra_columns
|
|
251
|
+
if infer_vega_type_from_data(data, column) == "quantitative"
|
|
252
|
+
]
|
|
253
|
+
if not numeric_extra_columns:
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
x_values = [row.get(x_field) for row in data]
|
|
257
|
+
if not _forms_gapless_integer_run(x_values):
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
raise ChartDataError.from_code(
|
|
261
|
+
ERR_HISTOGRAM_PREAGGREGATED,
|
|
262
|
+
chart_id=chart.id or "unknown",
|
|
263
|
+
field=x_field,
|
|
264
|
+
count_fields=", ".join(repr(column) for column in numeric_extra_columns),
|
|
265
|
+
)
|
|
@@ -40,6 +40,7 @@ def _trace_vl_spec(chart_id: str, renderer: str, spec: dict[str, Any]) -> None:
|
|
|
40
40
|
from dataface.core.compile.models.chart.resolved import ResolvedChart
|
|
41
41
|
from dataface.core.compile.models.chart.resolved._base import (
|
|
42
42
|
_CartesianResolvedChartFields,
|
|
43
|
+
_SharedResolvedChartFields,
|
|
43
44
|
)
|
|
44
45
|
from dataface.core.compile.models.chart.resolved.bar import ResolvedBarChart
|
|
45
46
|
from dataface.core.compile.models.chart.resolved.pie import ResolvedPieChart
|
|
@@ -98,6 +99,10 @@ def _render_vl_artifact(
|
|
|
98
99
|
)
|
|
99
100
|
if svg is not None:
|
|
100
101
|
return RenderArtifact(kind="svg", payload=svg)
|
|
102
|
+
# render_svg_family only returns None for the VL families, all of which
|
|
103
|
+
# are _SharedResolvedChartFields subclasses (KPI/callout/table/spark_bar
|
|
104
|
+
# return svg above) — title_style is guaranteed present below.
|
|
105
|
+
assert isinstance(resolved, _SharedResolvedChartFields)
|
|
101
106
|
|
|
102
107
|
effective_height = height
|
|
103
108
|
if (
|
|
@@ -144,13 +149,24 @@ def _render_vl_artifact(
|
|
|
144
149
|
if padding is not None:
|
|
145
150
|
vl["padding"] = padding
|
|
146
151
|
_apply_facet_layout(vl, render_data, width, effective_height)
|
|
147
|
-
apply_title_overflow_to_spec(vl,
|
|
152
|
+
apply_title_overflow_to_spec(vl, resolved.title_style)
|
|
148
153
|
_trace_vl_spec(resolved.id, "v2", vl)
|
|
149
154
|
return RenderArtifact(kind="vega_spec", payload=vl)
|
|
150
155
|
if "hconcat" in vl:
|
|
151
156
|
size_target = vl["hconcat"][0]
|
|
152
157
|
if "title" in vl:
|
|
153
158
|
size_target["title"] = vl.pop("title")
|
|
159
|
+
# hconcat's title wrap is deferred to render_vega_spec (see the
|
|
160
|
+
# comment below) — stamp the chart's own title_style so the deferred
|
|
161
|
+
# wrap reads the same chart-local source as the non-hconcat path
|
|
162
|
+
# above, instead of re-deriving it from the board-level style bag.
|
|
163
|
+
# A plain dict, not the live TitleStyle object: this sentinel can
|
|
164
|
+
# leak into a raw spec dict handed directly to vl-convert (e.g.
|
|
165
|
+
# generate_vega_lite_spec()'s return value, or a RenderArtifact
|
|
166
|
+
# payload used outside render_chart_artifact) — an unrecognized
|
|
167
|
+
# dict-valued key is harmless there, but a Pydantic object is not
|
|
168
|
+
# JSON-serializable.
|
|
169
|
+
vl["$df_title_style"] = resolved.title_style.model_dump()
|
|
154
170
|
else:
|
|
155
171
|
size_target = vl
|
|
156
172
|
if padding is not None:
|
|
@@ -179,7 +195,7 @@ def _render_vl_artifact(
|
|
|
179
195
|
# still the full column width — wrapping against it now overstates
|
|
180
196
|
# the room the title actually has. render_vega_spec wraps it after
|
|
181
197
|
# _correct_concat_overshoot shrinks pane[0] to its real width.
|
|
182
|
-
apply_title_overflow_to_spec(size_target,
|
|
198
|
+
apply_title_overflow_to_spec(size_target, resolved.title_style)
|
|
183
199
|
_trace_vl_spec(resolved.id, "v2", vl)
|
|
184
200
|
return RenderArtifact(kind="vega_spec", payload=vl)
|
|
185
201
|
|
|
@@ -424,12 +440,9 @@ def generate_vega_lite_spec(
|
|
|
424
440
|
SVG-family charts (kpi, table, spark_bar, callout) have no Vega-Lite
|
|
425
441
|
spec; callers that ask for one get ERR_VEGA_LITE_UNSUPPORTED_TYPE.
|
|
426
442
|
"""
|
|
427
|
-
import dataclasses
|
|
428
|
-
|
|
429
443
|
from dataface.core.compile.config import get_theme_style
|
|
430
444
|
from dataface.core.compile.models.style.resolved import resolve_style
|
|
431
445
|
from dataface.core.compile.resolve import preferred_chart_width, resolve
|
|
432
|
-
from dataface.core.compile.style_cascade import build_resolved_style
|
|
433
446
|
|
|
434
447
|
style = board_style if board_style is not None else resolve_style(get_theme_style())
|
|
435
448
|
# resolve() dispatches on chart.type first and raises for unsupported/synthetic
|
|
@@ -442,14 +455,13 @@ def generate_vega_lite_spec(
|
|
|
442
455
|
if isinstance(resolved, ResolvedPieChart)
|
|
443
456
|
else width if width is not None else preferred_chart_width(chart, style)
|
|
444
457
|
)
|
|
445
|
-
#
|
|
446
|
-
#
|
|
447
|
-
|
|
448
|
-
render_style = dataclasses.replace(style, charts=chart_charts_style)
|
|
458
|
+
# The resolved chart already carries its final chart-local presentation
|
|
459
|
+
# values (background, title_style) baked in at resolve() — no second
|
|
460
|
+
# style decision here. style is the unchanged face-level style.
|
|
449
461
|
artifact = _render_vl_artifact(
|
|
450
462
|
resolved,
|
|
451
463
|
data,
|
|
452
|
-
|
|
464
|
+
style,
|
|
453
465
|
width=resolved_width,
|
|
454
466
|
height=height,
|
|
455
467
|
is_placeholder=False,
|
|
@@ -462,7 +474,14 @@ def generate_vega_lite_spec(
|
|
|
462
474
|
)
|
|
463
475
|
assert isinstance(artifact.payload, dict)
|
|
464
476
|
vl = _apply_data_table_strip(artifact.payload, resolved, style.charts, data)
|
|
465
|
-
|
|
477
|
+
result = vl if vl is not None else artifact.payload
|
|
478
|
+
# $df_title_style is an internal hand-off to render_vega_spec's deferred
|
|
479
|
+
# hconcat title wrap (converters/chart.py) — this entry point returns the
|
|
480
|
+
# spec directly without going through that conversion, and the sentinel
|
|
481
|
+
# holds a non-JSON-serializable TitleStyle object, so it must not leak
|
|
482
|
+
# into the public return value.
|
|
483
|
+
result.pop("$df_title_style", None)
|
|
484
|
+
return result
|
|
466
485
|
|
|
467
486
|
|
|
468
487
|
def render_chart(
|