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
|
@@ -12,7 +12,7 @@ import re
|
|
|
12
12
|
from dataclasses import dataclass
|
|
13
13
|
from typing import TYPE_CHECKING, Any
|
|
14
14
|
|
|
15
|
-
from dataface.core.
|
|
15
|
+
from dataface.core.colors import (
|
|
16
16
|
is_sanitizable_color,
|
|
17
17
|
sanitize_color,
|
|
18
18
|
)
|
|
@@ -23,7 +23,9 @@ from dataface.core.compile.models.chart.authored import (
|
|
|
23
23
|
from dataface.core.compile.models.primitives import FontStyle
|
|
24
24
|
from dataface.core.compile.models.style.authored import (
|
|
25
25
|
PaginationConfig,
|
|
26
|
+
fill_table_column_defaults,
|
|
26
27
|
)
|
|
28
|
+
from dataface.core.compile.resolve import infer_pivot_measure_names
|
|
27
29
|
|
|
28
30
|
if TYPE_CHECKING:
|
|
29
31
|
from dataface.core.compile.models.chart.authored import (
|
|
@@ -71,7 +73,6 @@ from dataface.core.render.chart.table_support import (
|
|
|
71
73
|
is_total_role,
|
|
72
74
|
measure_column_demands,
|
|
73
75
|
measure_column_word_floors,
|
|
74
|
-
parse_table_column_configs,
|
|
75
76
|
resolve_cell_conditional_styles,
|
|
76
77
|
resolve_cell_glyph,
|
|
77
78
|
resolve_cell_glyph_from_overrides,
|
|
@@ -2489,6 +2490,7 @@ def _render_pagination_controls(
|
|
|
2489
2490
|
def _transpose_data_for_render(
|
|
2490
2491
|
formats: dict[str, str] | None,
|
|
2491
2492
|
columns: dict[str, TableColumnConfig] | None,
|
|
2493
|
+
column_defaults: TableColumnDefaultsConfig | None,
|
|
2492
2494
|
data: list[dict[str, Any]],
|
|
2493
2495
|
) -> tuple[list[dict[str, Any]], dict[str, TableColumnConfig]]:
|
|
2494
2496
|
"""Pivot a single wide row into N (label, value) rows for the normal renderer.
|
|
@@ -2498,6 +2500,15 @@ def _transpose_data_for_render(
|
|
|
2498
2500
|
- pivoted_columns maps "__metric__" (40% width) and "__value__" (60%, right-align)
|
|
2499
2501
|
with "Metric" / "Value" header labels.
|
|
2500
2502
|
|
|
2503
|
+
``columns`` (the resolved, source-column-keyed mapping — column_defaults
|
|
2504
|
+
already baked in there for the *source* columns) supplies each row's
|
|
2505
|
+
label/format only. The two output columns below are a render-native key
|
|
2506
|
+
space — they don't exist until this transform runs — so ``column_defaults``
|
|
2507
|
+
is applied to them here via ``fill_table_column_defaults``, filling only
|
|
2508
|
+
whichever fields "Metric"/"Value"/40%-width/right-align don't already
|
|
2509
|
+
set — "__value__" has no width of its own, so ``column_defaults.width``
|
|
2510
|
+
does pin it if authored.
|
|
2511
|
+
|
|
2501
2512
|
Raises ChartDataError when data does not have exactly one row.
|
|
2502
2513
|
"""
|
|
2503
2514
|
if len(data) != 1:
|
|
@@ -2525,8 +2536,12 @@ def _transpose_data_for_render(
|
|
|
2525
2536
|
pivoted.append({"__metric__": label, "__value__": formatted})
|
|
2526
2537
|
|
|
2527
2538
|
pivoted_columns: dict[str, TableColumnConfig] = {
|
|
2528
|
-
"__metric__":
|
|
2529
|
-
|
|
2539
|
+
"__metric__": fill_table_column_defaults(
|
|
2540
|
+
TableColumnConfig(label="Metric", width="40%"), column_defaults
|
|
2541
|
+
),
|
|
2542
|
+
"__value__": fill_table_column_defaults(
|
|
2543
|
+
TableColumnConfig(label="Value", align="right"), column_defaults
|
|
2544
|
+
),
|
|
2530
2545
|
}
|
|
2531
2546
|
return pivoted, pivoted_columns
|
|
2532
2547
|
|
|
@@ -2564,7 +2579,9 @@ def _render_table_svg_core(
|
|
|
2564
2579
|
tc = table_style.table
|
|
2565
2580
|
|
|
2566
2581
|
# Chart-level pivot (rows/columns/values channels): reshape long → wide.
|
|
2567
|
-
#
|
|
2582
|
+
# Called unconditionally — a flat table (no pivot_columns) and empty data
|
|
2583
|
+
# both pass straight through, so the layout sizer can call the same way and
|
|
2584
|
+
# can't disagree with us about which shapes reshape.
|
|
2568
2585
|
# groups is None for flat tables and single-dim single-measure pivot;
|
|
2569
2586
|
# list-of-levels otherwise (triggers N-row header rendering below).
|
|
2570
2587
|
# row_role_spec is hoisted here (ahead of the row loop that also reads it) so
|
|
@@ -2572,16 +2589,13 @@ def _render_table_svg_core(
|
|
|
2572
2589
|
# row. TableRowStyle.role defaults to None — resolve_row_role/is_total_role
|
|
2573
2590
|
# both accept None (treat every row as "value"), so unset is a valid case.
|
|
2574
2591
|
row_role_spec = tc.row.role
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
values=values,
|
|
2583
|
-
row_role_spec=row_role_spec,
|
|
2584
|
-
)
|
|
2592
|
+
data, _pivot_groups, _pivot_effective_values = pivot_table_data(
|
|
2593
|
+
data,
|
|
2594
|
+
rows=rows or [],
|
|
2595
|
+
columns=pivot_columns,
|
|
2596
|
+
values=values,
|
|
2597
|
+
row_role_spec=row_role_spec,
|
|
2598
|
+
)
|
|
2585
2599
|
|
|
2586
2600
|
# Build colors dict from chart-local TableChartStyle (which already has
|
|
2587
2601
|
# board-level values cascaded in). board_style is only needed for font
|
|
@@ -2730,14 +2744,9 @@ def _render_table_svg_core(
|
|
|
2730
2744
|
subtitle_lines = list(title_block.subtitle_lines)
|
|
2731
2745
|
subtitle_font_size = title_block.subtitle_font_size
|
|
2732
2746
|
|
|
2733
|
-
#
|
|
2734
|
-
#
|
|
2735
|
-
|
|
2736
|
-
column_configs = parse_table_column_configs(
|
|
2737
|
-
columns_promoted,
|
|
2738
|
-
column_defaults_promoted,
|
|
2739
|
-
query_columns=query_columns,
|
|
2740
|
-
)
|
|
2747
|
+
# columns_promoted is already the complete, final per-column config
|
|
2748
|
+
# mapping (defaults + explicit overrides + FK links baked in at resolve).
|
|
2749
|
+
column_configs: dict[str, TableColumnConfig] = dict(columns_promoted or {})
|
|
2741
2750
|
|
|
2742
2751
|
# Tables read conditional_formatting rules directly at render time — no
|
|
2743
2752
|
# internal lowering into column configs.
|
|
@@ -2764,8 +2773,13 @@ def _render_table_svg_core(
|
|
|
2764
2773
|
if existing is not None:
|
|
2765
2774
|
_expanded_cc[leaf] = existing
|
|
2766
2775
|
else:
|
|
2767
|
-
|
|
2768
|
-
|
|
2776
|
+
# Measure not explicitly authored — its key space (this leaf)
|
|
2777
|
+
# only exists after the pivot transform above, so resolve
|
|
2778
|
+
# excluded it from column_configs; apply column_defaults here.
|
|
2779
|
+
_expanded_cc[leaf] = fill_table_column_defaults(
|
|
2780
|
+
None,
|
|
2781
|
+
column_defaults_promoted,
|
|
2782
|
+
fallback_label=slug_to_text(_display_label),
|
|
2769
2783
|
)
|
|
2770
2784
|
column_configs = {**column_configs, **_expanded_cc}
|
|
2771
2785
|
# Remap column_when_rules: measure-name key → leaf keys.
|
|
@@ -2776,14 +2790,37 @@ def _render_table_svg_core(
|
|
|
2776
2790
|
if _measure_part and _measure_part in column_when_rules:
|
|
2777
2791
|
_expanded_cwr[leaf] = column_when_rules[_measure_part]
|
|
2778
2792
|
column_when_rules = {**column_when_rules, **_expanded_cwr}
|
|
2793
|
+
elif pivot_columns and data:
|
|
2794
|
+
# Single-dim single-measure pivot: leaf keys are the bare pivoted
|
|
2795
|
+
# values themselves (no separator, no measure-name lookup needed) —
|
|
2796
|
+
# but that key space still only exists after the pivot transform
|
|
2797
|
+
# above, so any key not already explicitly authored gets
|
|
2798
|
+
# column_defaults applied here, same as the multi-measure case.
|
|
2799
|
+
for leaf in data[0]:
|
|
2800
|
+
if leaf == row_role_spec or leaf in column_configs:
|
|
2801
|
+
continue
|
|
2802
|
+
column_configs[leaf] = fill_table_column_defaults(
|
|
2803
|
+
None, column_defaults_promoted, fallback_label=slug_to_text(leaf)
|
|
2804
|
+
)
|
|
2779
2805
|
|
|
2780
2806
|
header_overflow = resolve_header_overflow(table_config, header_overflow_promoted)
|
|
2781
2807
|
|
|
2782
2808
|
# Prefer explicit configured columns so helper/style columns can stay hidden.
|
|
2783
2809
|
# Dict keys preserve insertion order, so authored column order is honored.
|
|
2784
|
-
#
|
|
2785
|
-
#
|
|
2786
|
-
|
|
2810
|
+
#
|
|
2811
|
+
# In pivot mode, columns_promoted's keys are the real leaf/pivoted-value
|
|
2812
|
+
# keys ONLY for an explicitly-authored single-dim single-measure pivot
|
|
2813
|
+
# (authors key style.columns by those bare values directly) — checked via
|
|
2814
|
+
# the subset test below. For every other pivot shape (multi-measure/dim,
|
|
2815
|
+
# or resolve's inferred row/pivot-dimension columns) columns_promoted is
|
|
2816
|
+
# keyed by measure or dimension names that never match the actual
|
|
2817
|
+
# post-transform keys, so the visible list must come from data instead.
|
|
2818
|
+
# An empty `data` means the transform above never ran (nothing to check
|
|
2819
|
+
# against) — honor the authored list so an authored table still shows
|
|
2820
|
+
# its declared header in the empty-state render.
|
|
2821
|
+
if columns_promoted and (
|
|
2822
|
+
not pivot_columns or not data or set(columns_promoted).issubset(data[0])
|
|
2823
|
+
):
|
|
2787
2824
|
columns = list(columns_promoted.keys())
|
|
2788
2825
|
else:
|
|
2789
2826
|
columns = list(data[0].keys()) if data else []
|
|
@@ -3397,7 +3434,9 @@ def render_table_svg(
|
|
|
3397
3434
|
columns = chart.columns
|
|
3398
3435
|
|
|
3399
3436
|
if chart.style.table.transpose and data:
|
|
3400
|
-
data, columns = _transpose_data_for_render(
|
|
3437
|
+
data, columns = _transpose_data_for_render(
|
|
3438
|
+
chart.style.formats, columns, chart.column_defaults, data
|
|
3439
|
+
)
|
|
3401
3440
|
|
|
3402
3441
|
return _render_table_svg_core(
|
|
3403
3442
|
chart_id=chart.id,
|
|
@@ -3479,9 +3518,8 @@ def _validate_pivot_fields(
|
|
|
3479
3518
|
f" carries the per-row role — observed keys: {observed}"
|
|
3480
3519
|
)
|
|
3481
3520
|
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
values if values is not None else [k for k in all_keys if k not in claimed]
|
|
3521
|
+
effective_values = infer_pivot_measure_names(
|
|
3522
|
+
all_keys, rows, columns, values, row_role_spec
|
|
3485
3523
|
)
|
|
3486
3524
|
for v in effective_values:
|
|
3487
3525
|
if v not in all_keys:
|
|
@@ -3548,12 +3586,18 @@ def pivot_table_data(
|
|
|
3548
3586
|
data: list[dict[str, Any]],
|
|
3549
3587
|
*,
|
|
3550
3588
|
rows: list[str],
|
|
3551
|
-
columns: list[str],
|
|
3589
|
+
columns: list[str] | None,
|
|
3552
3590
|
values: list[str] | None,
|
|
3553
3591
|
row_role_spec: str | None = None,
|
|
3554
3592
|
) -> tuple[list[dict[str, Any]], list[list[tuple[str, int, int]]] | None, list[str]]:
|
|
3555
3593
|
"""Reshape long-form tidy data into a wide pivot matrix.
|
|
3556
3594
|
|
|
3595
|
+
Also the single owner of the "is this a pivot?" decision: no ``columns``
|
|
3596
|
+
means a flat table and the rows pass through untouched. The renderer and
|
|
3597
|
+
the layout sizer (``layout_sizing._get_table_height_from_data``) both call
|
|
3598
|
+
unconditionally, so the height the sizer reserves is always the row count
|
|
3599
|
+
the renderer draws.
|
|
3600
|
+
|
|
3557
3601
|
Args:
|
|
3558
3602
|
data: Long-form source rows.
|
|
3559
3603
|
rows: Row-dimension field names (form the composite row key). When
|
|
@@ -3562,7 +3606,8 @@ def pivot_table_data(
|
|
|
3562
3606
|
column order (mirrors ``values``'s own inference).
|
|
3563
3607
|
columns: Pivot-column field(s) whose distinct values become column headers.
|
|
3564
3608
|
Single field → single-dimension pivot. Multiple fields → nested
|
|
3565
|
-
multi-dimension pivot (outer dim first, inner dim last).
|
|
3609
|
+
multi-dimension pivot (outer dim first, inner dim last). Empty or
|
|
3610
|
+
``None`` → not a pivot; ``data`` comes back unchanged.
|
|
3566
3611
|
values: Measure field names. When ``None``, infers as all query columns
|
|
3567
3612
|
not in ``rows``, not any column field, and not the role marker,
|
|
3568
3613
|
preserving query column order.
|
|
@@ -3610,8 +3655,8 @@ def pivot_table_data(
|
|
|
3610
3655
|
field onto rows or columns." (a total row keys by rows-dim like any
|
|
3611
3656
|
other, so this covers both detail and total collisions).
|
|
3612
3657
|
"""
|
|
3613
|
-
if not data:
|
|
3614
|
-
return
|
|
3658
|
+
if not columns or not data:
|
|
3659
|
+
return data, None, []
|
|
3615
3660
|
|
|
3616
3661
|
# Both dims omitted → the remaining fields can't be split into rows vs
|
|
3617
3662
|
# measures without guessing. Validate-and-error rather than degenerate into
|
|
@@ -29,6 +29,7 @@ from dataface.core.render.chart.title_overflow import (
|
|
|
29
29
|
from dataface.core.render.format_utils import default_number_format, format_value
|
|
30
30
|
from dataface.core.render.utils import resolve_tone_color, slug_to_text
|
|
31
31
|
from dataface.core.text.case import CaseValue, apply_case
|
|
32
|
+
from dataface.core.text.format_d3 import portable_strftime
|
|
32
33
|
from mdsvg.fonts import FontMeasurer
|
|
33
34
|
|
|
34
35
|
# Three-lane numeric rendering constants used by table.py:_compute_lane_positions.
|
|
@@ -49,7 +50,6 @@ if TYPE_CHECKING:
|
|
|
49
50
|
TableColumnConfig,
|
|
50
51
|
)
|
|
51
52
|
from dataface.core.compile.models.primitives import FormatConfig
|
|
52
|
-
from dataface.core.compile.models.style.authored import TableColumnDefaultsConfig
|
|
53
53
|
from dataface.core.compile.models.style.theme import (
|
|
54
54
|
KpiTonesStyle,
|
|
55
55
|
TableChartStyle,
|
|
@@ -212,62 +212,11 @@ def _format_temporal_value(
|
|
|
212
212
|
d = value
|
|
213
213
|
|
|
214
214
|
try:
|
|
215
|
-
return d
|
|
215
|
+
return portable_strftime(d, format_spec)
|
|
216
216
|
except ValueError as e:
|
|
217
217
|
raise ValueError(f"Invalid temporal format spec {format_spec!r}: {e}") from e
|
|
218
218
|
|
|
219
219
|
|
|
220
|
-
def _apply_column_defaults(
|
|
221
|
-
cfg: TableColumnConfig,
|
|
222
|
-
defaults: TableColumnDefaultsConfig,
|
|
223
|
-
) -> TableColumnConfig:
|
|
224
|
-
updates = {
|
|
225
|
-
name: getattr(defaults, name)
|
|
226
|
-
for name in type(defaults).model_fields
|
|
227
|
-
if getattr(cfg, name) is None and getattr(defaults, name) is not None
|
|
228
|
-
}
|
|
229
|
-
return cfg.model_copy(update=updates) if updates else cfg
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def parse_table_column_configs(
|
|
233
|
-
columns_config: dict[str, TableColumnConfig] | None,
|
|
234
|
-
defaults: TableColumnDefaultsConfig | None = None,
|
|
235
|
-
query_columns: list[str] | None = None,
|
|
236
|
-
) -> dict[str, TableColumnConfig]:
|
|
237
|
-
"""Apply table-level defaults to the resolved column configurations.
|
|
238
|
-
|
|
239
|
-
When column_defaults is set and columns is unset, query_columns is used to
|
|
240
|
-
materialize a default config for every query-inferred column — enabling the
|
|
241
|
-
"no columns list" authoring shape where uniform presentation is set once at
|
|
242
|
-
the table style level.
|
|
243
|
-
"""
|
|
244
|
-
from dataface.core.compile.models.chart.authored import (
|
|
245
|
-
SparkConfig,
|
|
246
|
-
TableColumnConfig,
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
# No explicit columns: if column_defaults is set, materialize defaults for
|
|
250
|
-
# every query-inferred column so authors need not list columns at all.
|
|
251
|
-
if not columns_config:
|
|
252
|
-
if defaults and query_columns:
|
|
253
|
-
return {
|
|
254
|
-
col: _apply_column_defaults(TableColumnConfig(), defaults)
|
|
255
|
-
for col in query_columns
|
|
256
|
-
}
|
|
257
|
-
return {}
|
|
258
|
-
|
|
259
|
-
result: dict[str, TableColumnConfig] = {}
|
|
260
|
-
for field_name, col_cfg in columns_config.items():
|
|
261
|
-
cfg = col_cfg
|
|
262
|
-
# Normalize spark string shorthand to SparkConfig so render code can
|
|
263
|
-
# use isinstance(cfg.spark, SparkConfig) uniformly.
|
|
264
|
-
if isinstance(cfg.spark, str):
|
|
265
|
-
cfg = cfg.model_copy(update={"spark": SparkConfig(type=cfg.spark)})
|
|
266
|
-
result[field_name] = _apply_column_defaults(cfg, defaults) if defaults else cfg
|
|
267
|
-
|
|
268
|
-
return result
|
|
269
|
-
|
|
270
|
-
|
|
271
220
|
def parse_column_width(
|
|
272
221
|
width_hint: int | str | None, available_width: float
|
|
273
222
|
) -> float | None:
|
|
@@ -30,9 +30,10 @@ import re
|
|
|
30
30
|
import statistics
|
|
31
31
|
from typing import Any
|
|
32
32
|
|
|
33
|
-
from dataface.core.compile.enrich import is_year_shaped
|
|
34
33
|
from dataface.core.font_measure import FontMeasurer
|
|
35
34
|
from dataface.core.render.chart.artifacts import ChartRenderData
|
|
35
|
+
from dataface.core.text.format_d3 import portable_strftime
|
|
36
|
+
from dataface.core.utils import is_year_shaped
|
|
36
37
|
|
|
37
38
|
# ISO date: "2024-01-15"
|
|
38
39
|
_ISO_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
|
@@ -227,7 +228,7 @@ def _cadence_token_width(
|
|
|
227
228
|
if format_tu == "yearweek":
|
|
228
229
|
return measurer.measure(d.strftime("W%V"), size)
|
|
229
230
|
if format_tu == "yearmonthdate":
|
|
230
|
-
return measurer.measure(d
|
|
231
|
+
return measurer.measure(portable_strftime(d, "%-d %b"), size)
|
|
231
232
|
raise ValueError(
|
|
232
233
|
f"_cadence_token_width: {format_tu!r} is not a bucketed calendar grain"
|
|
233
234
|
)
|
|
@@ -21,10 +21,12 @@ the standard path like any other sub-spec.
|
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
23
|
import json
|
|
24
|
+
from collections.abc import Callable
|
|
24
25
|
from typing import Any
|
|
25
26
|
|
|
26
27
|
from dataface.core.compile.models.style.resolved import ResolvedStyle
|
|
27
28
|
from dataface.core.compile.vega_lite import VEGA_LITE_SCHEMA_URL
|
|
29
|
+
from dataface.core.render.chart._types import VLDict
|
|
28
30
|
from dataface.core.render.chart.presentation import (
|
|
29
31
|
_HREF_FIELD,
|
|
30
32
|
apply_presentation_defaults,
|
|
@@ -411,6 +413,25 @@ def _wrap_vconcat_label_rail(
|
|
|
411
413
|
# ---------------------------------------------------------------------------
|
|
412
414
|
|
|
413
415
|
|
|
416
|
+
def _in_chart_pane(vl: VLDict, stamp: Callable[[VLDict], VLDict]) -> VLDict:
|
|
417
|
+
"""Run ``stamp`` on the pane that carries the marks.
|
|
418
|
+
|
|
419
|
+
An endpoint-label rail wraps the chart in a concat: ``hconcat`` for the
|
|
420
|
+
vertical layout (chart first, label pane right), ``vconcat`` for the
|
|
421
|
+
horizontal one (rail first, chart second). Neither VL nor Vega reads a
|
|
422
|
+
top-level ``encoding``, ``transform`` or ``params`` on a concat spec, so
|
|
423
|
+
anything that stamps those has to descend to the chart pane or it lands
|
|
424
|
+
somewhere the renderer never looks.
|
|
425
|
+
"""
|
|
426
|
+
if "hconcat" in vl:
|
|
427
|
+
main, *rest = vl["hconcat"]
|
|
428
|
+
return {**vl, "hconcat": [_in_chart_pane(main, stamp), *rest]}
|
|
429
|
+
if "vconcat" in vl:
|
|
430
|
+
rail, main = vl["vconcat"]
|
|
431
|
+
return {**vl, "vconcat": [rail, _in_chart_pane(main, stamp)]}
|
|
432
|
+
return stamp(vl)
|
|
433
|
+
|
|
434
|
+
|
|
414
435
|
def _apply_structured_tooltip(vl: dict[str, Any], expr: str) -> dict[str, Any]:
|
|
415
436
|
"""Inject the description encoding for the structured tooltip.
|
|
416
437
|
|
|
@@ -433,11 +454,13 @@ def _apply_structured_tooltip(vl: dict[str, Any], expr: str) -> dict[str, Any]:
|
|
|
433
454
|
Vega signal expression (confirmed: identical rendered aria-label content,
|
|
434
455
|
clean impute groupby with the fix, corrupted path without it).
|
|
435
456
|
|
|
436
|
-
Endpoint labels
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
457
|
+
Endpoint labels wrap the layered spec in an ``hconcat`` (right_pane,
|
|
458
|
+
vertical stacked bar/line/area, chart pane at index 0) or a ``vconcat``
|
|
459
|
+
(top_rail, horizontal stacked bar, chart pane at index 1 — the rail sits
|
|
460
|
+
above it) before this post-processing step runs (``_translate_layered``
|
|
461
|
+
calls ``_wrap_endpoint_labels`` internally) — recurse into the actual
|
|
462
|
+
chart pane rather than stamping a no-op encoding onto the wrapper itself;
|
|
463
|
+
the label pane/rail carries no data marks of its own.
|
|
441
464
|
|
|
442
465
|
A layered spec needs one more override for ``BaselineFeature``'s
|
|
443
466
|
zero/top/unity rule overlays specifically: they're appended sub-layers
|
|
@@ -457,41 +480,47 @@ def _apply_structured_tooltip(vl: dict[str, Any], expr: str) -> dict[str, Any]:
|
|
|
457
480
|
channel" convention — the same one ``_full_rule_at`` and
|
|
458
481
|
``_isolate_independent_y_axis`` already rely on for ``axis``/``legend``).
|
|
459
482
|
"""
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
result
|
|
483
|
+
|
|
484
|
+
def stamp(pane: VLDict) -> VLDict:
|
|
485
|
+
result = dict(pane)
|
|
486
|
+
enc = dict(result.get("encoding") or {})
|
|
487
|
+
enc["description"] = {"value": {"expr": expr}}
|
|
488
|
+
result["encoding"] = enc
|
|
489
|
+
layers = result.get("layer")
|
|
490
|
+
if isinstance(layers, list):
|
|
491
|
+
result["layer"] = [
|
|
492
|
+
(
|
|
493
|
+
{
|
|
494
|
+
**layer,
|
|
495
|
+
"encoding": {
|
|
496
|
+
**(layer.get("encoding") or {}),
|
|
497
|
+
"description": None,
|
|
498
|
+
},
|
|
499
|
+
}
|
|
500
|
+
if "data" in layer
|
|
501
|
+
else layer
|
|
502
|
+
)
|
|
503
|
+
for layer in layers
|
|
504
|
+
]
|
|
464
505
|
return result
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
enc["description"] = {"value": {"expr": expr}}
|
|
468
|
-
result["encoding"] = enc
|
|
469
|
-
layers = result.get("layer")
|
|
470
|
-
if isinstance(layers, list):
|
|
471
|
-
result["layer"] = [
|
|
472
|
-
(
|
|
473
|
-
{
|
|
474
|
-
**layer,
|
|
475
|
-
"encoding": {**(layer.get("encoding") or {}), "description": None},
|
|
476
|
-
}
|
|
477
|
-
if "data" in layer
|
|
478
|
-
else layer
|
|
479
|
-
)
|
|
480
|
-
for layer in layers
|
|
481
|
-
]
|
|
482
|
-
return result
|
|
506
|
+
|
|
507
|
+
return _in_chart_pane(vl, stamp)
|
|
483
508
|
|
|
484
509
|
|
|
485
510
|
def _apply_href_link(vl: dict[str, Any], href_link: str) -> dict[str, Any]:
|
|
486
511
|
"""Inject calculate transform + href encoding for click interactivity."""
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
512
|
+
|
|
513
|
+
def stamp(pane: VLDict) -> VLDict:
|
|
514
|
+
result = dict(pane)
|
|
515
|
+
transforms = list(result.get("transform") or [])
|
|
516
|
+
transforms.append({"calculate": href_link, "as": _HREF_FIELD})
|
|
517
|
+
result["transform"] = transforms
|
|
518
|
+
enc = dict(result.get("encoding") or {})
|
|
519
|
+
enc["href"] = {"field": _HREF_FIELD, "type": "nominal"}
|
|
520
|
+
result["encoding"] = enc
|
|
521
|
+
return result
|
|
522
|
+
|
|
523
|
+
return _in_chart_pane(vl, stamp)
|
|
495
524
|
|
|
496
525
|
|
|
497
526
|
def _apply_legend_toggle(vl: dict[str, Any], color_field: str) -> dict[str, Any]:
|
|
@@ -500,9 +529,12 @@ def _apply_legend_toggle(vl: dict[str, Any], color_field: str) -> dict[str, Any]
|
|
|
500
529
|
In a layered spec the param is anchored inside the first data-bearing layer
|
|
501
530
|
to avoid duplicate Vega signals.
|
|
502
531
|
"""
|
|
532
|
+
if "hconcat" in vl or "vconcat" in vl:
|
|
533
|
+
return _in_chart_pane(vl, lambda pane: _apply_legend_toggle(pane, color_field))
|
|
534
|
+
|
|
503
535
|
enc = vl.get("encoding")
|
|
504
536
|
if not isinstance(enc, dict):
|
|
505
|
-
return vl
|
|
537
|
+
return vl
|
|
506
538
|
|
|
507
539
|
color_enc = enc.get("color")
|
|
508
540
|
if not isinstance(color_enc, dict):
|
|
@@ -626,12 +658,14 @@ def assemble_final_vl(
|
|
|
626
658
|
|
|
627
659
|
Args:
|
|
628
660
|
spec: ``ChartSpec`` produced by an emitter and mutated by the feature
|
|
629
|
-
pipeline. Must carry ``title``/``subtitle``/``title_font
|
|
630
|
-
|
|
631
|
-
chart
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
661
|
+
pipeline. Must carry ``title``/``subtitle``/``title_font``/
|
|
662
|
+
``background``/``title_style`` (set by
|
|
663
|
+
``FaceRenderSession.emit_chart`` from the resolved chart —
|
|
664
|
+
``title`` is the raw jinja-resolved text; title-case is applied
|
|
665
|
+
here from ``title_style.font.case``, VL-presentation only).
|
|
666
|
+
board_style: Fully resolved board-level style. Supplies only
|
|
667
|
+
``vega_config`` (baked theme config) — unchanged face-level
|
|
668
|
+
presentation, not a per-chart decision.
|
|
635
669
|
|
|
636
670
|
Returns:
|
|
637
671
|
Vega-Lite JSON-serialisable spec dict with config and background applied.
|
|
@@ -641,16 +675,20 @@ def assemble_final_vl(
|
|
|
641
675
|
"""
|
|
642
676
|
vl = translate_to_vl(spec)
|
|
643
677
|
|
|
678
|
+
assert spec.background is not None and spec.title_style is not None, (
|
|
679
|
+
"ChartSpec.background/title_style must be set by "
|
|
680
|
+
"FaceRenderSession.emit_chart before assemble_final_vl runs"
|
|
681
|
+
)
|
|
644
682
|
vl = apply_presentation_defaults(
|
|
645
683
|
vl,
|
|
646
|
-
|
|
684
|
+
spec.background,
|
|
647
685
|
board_style.vega_config,
|
|
648
686
|
)
|
|
649
687
|
|
|
650
688
|
if spec.title or spec.subtitle:
|
|
651
689
|
from dataface.core.render.chart.spec_builders import set_chart_title
|
|
652
690
|
|
|
653
|
-
set_chart_title(vl, spec.title, spec.subtitle,
|
|
691
|
+
set_chart_title(vl, spec.title, spec.subtitle, case=spec.title_style.font.case)
|
|
654
692
|
|
|
655
693
|
# config.title.fontSize/fontWeight come from theme YAML as floats; coerce to int.
|
|
656
694
|
if "config" in vl and "title" in vl["config"]:
|