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
|
@@ -22,9 +22,12 @@ from dataface.core.compile.models.style.resolved import (
|
|
|
22
22
|
build_resolved_axis,
|
|
23
23
|
)
|
|
24
24
|
from dataface.core.compile.resolve._axes import (
|
|
25
|
+
_author_asked_for_endpoint_labels,
|
|
26
|
+
_author_hid_legend,
|
|
25
27
|
_bake_ay_orient,
|
|
26
28
|
_bake_cartesian_axes,
|
|
27
29
|
_edge_or_none,
|
|
30
|
+
_endpoint_labels_off_for_layers,
|
|
28
31
|
_extract_axis_overrides,
|
|
29
32
|
_series_label_layout_for_width,
|
|
30
33
|
_suppress_legend_for_endpoint_labels,
|
|
@@ -204,17 +207,22 @@ def _resolve_area(
|
|
|
204
207
|
board_rcs = board_style.charts
|
|
205
208
|
chart_rcs = build_resolved_style(board_style, normalized)
|
|
206
209
|
area = chart_rcs.area
|
|
207
|
-
endpoint_labels, show_top_legend = _series_label_layout_for_width(
|
|
208
|
-
area.endpoint_labels, width
|
|
209
|
-
)
|
|
210
210
|
primary = _with_color_tokens(normalized.style, board_style)
|
|
211
|
+
endpoint_labels, tiny_top_legend = _series_label_layout_for_width(
|
|
212
|
+
_endpoint_labels_off_for_layers(
|
|
213
|
+
area.endpoint_labels,
|
|
214
|
+
normalized,
|
|
215
|
+
_author_asked_for_endpoint_labels(normalized.style),
|
|
216
|
+
),
|
|
217
|
+
width,
|
|
218
|
+
)
|
|
211
219
|
channels = _channels_for(normalized, data)
|
|
212
220
|
wide_measure_series = isinstance(normalized.y, list)
|
|
213
221
|
x_ch_type = _classify_to_channel_type(normalized.x, data, is_dimension=True)
|
|
214
222
|
resolved_stack = normalized.stack if normalized.stack is not None else area.stack
|
|
215
223
|
_validate_area_encoding(normalized, data, x_ch_type, resolved_stack)
|
|
216
224
|
ax_ov = _extract_axis_overrides(primary, normalized.x_label, normalized.y_label)
|
|
217
|
-
ax_merged, ay_merged = _bake_cartesian_axes(
|
|
225
|
+
ax_merged, ay_merged, ax_band_position, ay_band_position = _bake_cartesian_axes(
|
|
218
226
|
chart_rcs,
|
|
219
227
|
normalized,
|
|
220
228
|
"area",
|
|
@@ -232,7 +240,8 @@ def _resolve_area(
|
|
|
232
240
|
endpoint_labels.visible,
|
|
233
241
|
wide_measure_series=wide_measure_series,
|
|
234
242
|
)
|
|
235
|
-
|
|
243
|
+
_ay_cont_area = ay_merged.scale.continuous if ay_merged.scale is not None else None
|
|
244
|
+
authored_y_domain = _ay_cont_area.domain if _ay_cont_area is not None else None
|
|
236
245
|
stable_extent = _eligible_stable_extent(ay_merged, stable_y_domain)
|
|
237
246
|
_area_axis_values = list(stable_extent) if stable_extent else _area_floats
|
|
238
247
|
if y_field_area:
|
|
@@ -247,10 +256,15 @@ def _resolve_area(
|
|
|
247
256
|
# The layer ambiguity check must see the pre-workaround domain state: it is
|
|
248
257
|
# about an authored chart-level domain, not the synthetic area-log domain.
|
|
249
258
|
_reject_non_positive_log_scale_data(normalized.id, ay_merged, _log_y_fields, data)
|
|
259
|
+
_ay_cont_log = ay_merged.scale.continuous if ay_merged.scale is not None else None
|
|
250
260
|
if (
|
|
251
|
-
|
|
252
|
-
and
|
|
253
|
-
and resolved_stack
|
|
261
|
+
_ay_cont_log is not None
|
|
262
|
+
and _ay_cont_log.type == "log"
|
|
263
|
+
and resolved_stack
|
|
264
|
+
not in (
|
|
265
|
+
None,
|
|
266
|
+
"none",
|
|
267
|
+
)
|
|
254
268
|
):
|
|
255
269
|
raise CompilationError.from_code(
|
|
256
270
|
ERR_AREA_STACKED_LOG_SCALE_NOT_SUPPORTED,
|
|
@@ -259,9 +273,9 @@ def _resolve_area(
|
|
|
259
273
|
)
|
|
260
274
|
if (
|
|
261
275
|
_log_y_fields
|
|
262
|
-
and
|
|
263
|
-
and
|
|
264
|
-
and
|
|
276
|
+
and _ay_cont_log is not None
|
|
277
|
+
and _ay_cont_log.type == "log"
|
|
278
|
+
and _ay_cont_log.domain is None
|
|
265
279
|
):
|
|
266
280
|
# Vega-Lite's automatic domain inference degenerates for an area mark
|
|
267
281
|
# on a log scale with no explicit domain (verified: renders as a flat
|
|
@@ -275,11 +289,14 @@ def _resolve_area(
|
|
|
275
289
|
_log_y_floats = [
|
|
276
290
|
v for field in _log_y_fields for v in numeric_column_values(data, field)
|
|
277
291
|
]
|
|
278
|
-
if _log_y_floats:
|
|
292
|
+
if _log_y_floats and ay_merged.scale is not None:
|
|
293
|
+
_new_cont = _ay_cont_log.model_copy(
|
|
294
|
+
update={"domain": [min(_log_y_floats), max(_log_y_floats)]}
|
|
295
|
+
)
|
|
279
296
|
ay_merged = ay_merged.model_copy(
|
|
280
297
|
update={
|
|
281
298
|
"scale": ay_merged.scale.model_copy(
|
|
282
|
-
update={"
|
|
299
|
+
update={"continuous": _new_cont}
|
|
283
300
|
)
|
|
284
301
|
}
|
|
285
302
|
)
|
|
@@ -341,9 +358,10 @@ def _resolve_area(
|
|
|
341
358
|
area_domain_max = area_ticks.domain_max if _non_stacked_area else None
|
|
342
359
|
area_domain_min = area_ticks.domain_min if _non_stacked_area else None
|
|
343
360
|
# Area's x is always a bottom-orient temporal/ordinal axis — no left/right edge.
|
|
344
|
-
ax = build_resolved_axis(ax_merged, edge=None)
|
|
361
|
+
ax = build_resolved_axis(ax_merged, band_position=ax_band_position, edge=None)
|
|
345
362
|
ay = build_resolved_axis(
|
|
346
363
|
ay_merged,
|
|
364
|
+
band_position=ay_band_position,
|
|
347
365
|
edge=_edge_or_none(ay_merged.position),
|
|
348
366
|
tick_values=area_ticks.ticks,
|
|
349
367
|
domain_max=area_domain_max if area_domain_max is not None else ...,
|
|
@@ -382,7 +400,9 @@ def _resolve_area(
|
|
|
382
400
|
if line_overrides:
|
|
383
401
|
line_mark_merged = line_mark_merged.model_copy(update=line_overrides)
|
|
384
402
|
line_mark_merged = line_mark_merged.model_copy(
|
|
385
|
-
update={
|
|
403
|
+
update={
|
|
404
|
+
"labels": _label_format_fallback(line_mark_merged.labels, ay.labels.format)
|
|
405
|
+
}
|
|
386
406
|
)
|
|
387
407
|
# Density-adaptive stroke for area's top-edge line. For stacked charts the
|
|
388
408
|
# effective stroke is on the stacked perimeter (already merged above into
|
|
@@ -440,7 +460,7 @@ def _resolve_area(
|
|
|
440
460
|
# visually track the line width).
|
|
441
461
|
resolved_area_mark = _build_resolved_area_mark(area_mark_merged)
|
|
442
462
|
resolved_line_mark = _build_resolved_area_line(line_mark_merged)
|
|
443
|
-
area_style_tail = _cartesian_style_tail(board_rcs,
|
|
463
|
+
area_style_tail = _cartesian_style_tail(board_rcs, ax, ay)
|
|
444
464
|
_area_fmt = _measure_tooltip_format(normalized, primary, board_rcs)
|
|
445
465
|
if _area_fmt:
|
|
446
466
|
area_style_tail["tooltip_format"] = _area_fmt
|
|
@@ -459,12 +479,22 @@ def _resolve_area(
|
|
|
459
479
|
_effective_palette(board_rcs, primary),
|
|
460
480
|
requested_alias_palette=_effective_requested_alias_palette(board_rcs, primary),
|
|
461
481
|
automatic_link_candidate=automatic_link_candidate,
|
|
482
|
+
layout_padding=area.padding,
|
|
462
483
|
suppress_legend=_suppress_legend_for_endpoint_labels(
|
|
463
484
|
channels,
|
|
464
485
|
endpoint_labels.visible,
|
|
486
|
+
bool(normalized.layers),
|
|
465
487
|
wide_measure_series=wide_measure_series,
|
|
466
488
|
),
|
|
467
|
-
|
|
489
|
+
top_legend=(
|
|
490
|
+
"off"
|
|
491
|
+
if _author_hid_legend(primary)
|
|
492
|
+
else (
|
|
493
|
+
"compact"
|
|
494
|
+
if tiny_top_legend
|
|
495
|
+
else ("row" if normalized.layers else "off")
|
|
496
|
+
)
|
|
497
|
+
),
|
|
468
498
|
)
|
|
469
499
|
resolved_layers = _resolve_layer_list(
|
|
470
500
|
normalized.layers,
|
|
@@ -478,7 +508,7 @@ def _resolve_area(
|
|
|
478
508
|
_check_layers_y_domain(normalized.id, resolved_layers, authored_y_domain)
|
|
479
509
|
return ResolvedAreaChart(
|
|
480
510
|
**bkw,
|
|
481
|
-
**_cartesian_kwargs(normalized,
|
|
511
|
+
**_cartesian_kwargs(normalized, chart_rcs, variables),
|
|
482
512
|
chart_type="area",
|
|
483
513
|
stack=resolved_stack,
|
|
484
514
|
style=ResolvedAreaStyle(
|
|
@@ -7,25 +7,33 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
from dataface.core.compile.errors import CompilationError
|
|
9
9
|
from dataface.core.compile.merge import merge_onto_base
|
|
10
|
+
from dataface.core.compile.models.chart.authored._data_table import (
|
|
11
|
+
ChartDataTablePerSeries,
|
|
12
|
+
)
|
|
10
13
|
from dataface.core.compile.models.chart.normalized import (
|
|
11
14
|
BarChart,
|
|
12
15
|
)
|
|
13
16
|
from dataface.core.compile.models.chart.resolved import (
|
|
14
17
|
ResolvedBarChart,
|
|
15
18
|
)
|
|
19
|
+
from dataface.core.compile.models.style.authored import EndpointLabelsConfig
|
|
16
20
|
from dataface.core.compile.models.style.resolved import (
|
|
17
21
|
ResolvedBarStyle,
|
|
18
22
|
ResolvedStyle,
|
|
19
23
|
build_resolved_axis,
|
|
20
24
|
)
|
|
21
25
|
from dataface.core.compile.models.style.theme import (
|
|
22
|
-
|
|
26
|
+
BaseScaleStyle,
|
|
27
|
+
ScaleContinuousStyle,
|
|
23
28
|
)
|
|
24
29
|
from dataface.core.compile.resolve._axes import (
|
|
30
|
+
_author_asked_for_endpoint_labels,
|
|
31
|
+
_author_hid_legend,
|
|
25
32
|
_bake_ay_orient,
|
|
26
33
|
_bake_ay_position_left,
|
|
27
34
|
_bake_cartesian_axes,
|
|
28
35
|
_edge_or_none,
|
|
36
|
+
_endpoint_labels_off_for_layers,
|
|
29
37
|
_extract_axis_overrides,
|
|
30
38
|
_series_label_layout_for_width,
|
|
31
39
|
_suppress_legend_for_endpoint_labels,
|
|
@@ -86,6 +94,10 @@ from dataface.core.diagnostics.codes_compile import (
|
|
|
86
94
|
ERR_BAR_LOG_SCALE_NOT_SUPPORTED,
|
|
87
95
|
ERR_BAR_Y_NOT_NUMERIC,
|
|
88
96
|
)
|
|
97
|
+
from dataface.core.utils import (
|
|
98
|
+
numeric_column_values,
|
|
99
|
+
stacked_x_domain_order,
|
|
100
|
+
)
|
|
89
101
|
|
|
90
102
|
__all__ = [
|
|
91
103
|
"_resolve_bar",
|
|
@@ -93,6 +105,125 @@ __all__ = [
|
|
|
93
105
|
]
|
|
94
106
|
|
|
95
107
|
|
|
108
|
+
def _bar_endpoint_labels_for_stack(
|
|
109
|
+
endpoint_labels: EndpointLabelsConfig,
|
|
110
|
+
normalized: BarChart,
|
|
111
|
+
data: ChartRows,
|
|
112
|
+
stack_mode: str,
|
|
113
|
+
horizontal: bool,
|
|
114
|
+
author_opted_in: bool,
|
|
115
|
+
) -> EndpointLabelsConfig:
|
|
116
|
+
"""Direct labelling is the default; the disqualifiers below turn it back off.
|
|
117
|
+
|
|
118
|
+
Every disqualifier here steers the *default* away from a shape it would
|
|
119
|
+
render badly or not at all. An author who wrote
|
|
120
|
+
``style.endpoint_labels.visible: true`` on this chart has said they want
|
|
121
|
+
the rail on that shape, so ``author_opted_in`` returns the cascaded value
|
|
122
|
+
untouched: grouped bars label at their bar tops as they always have, and
|
|
123
|
+
the shapes render refuses (a negative measure, a sorted or centre-stacked
|
|
124
|
+
horizontal rail, ``multiples:``) raise there with a message naming the
|
|
125
|
+
conflict. Quietly dropping the rail instead would be neither.
|
|
126
|
+
"""
|
|
127
|
+
if author_opted_in:
|
|
128
|
+
return endpoint_labels
|
|
129
|
+
if normalized.data_table is not None and any(
|
|
130
|
+
isinstance(entry, ChartDataTablePerSeries) and not entry.by_measure
|
|
131
|
+
for entry in normalized.data_table.entries
|
|
132
|
+
):
|
|
133
|
+
# A per-series data table already prints one row per series, labelled
|
|
134
|
+
# in that series' own ink — the rail would name them a second time,
|
|
135
|
+
# and it costs the plot the height and the axis side it needs.
|
|
136
|
+
return endpoint_labels.model_copy(update={"visible": False})
|
|
137
|
+
if stack_mode == "none":
|
|
138
|
+
# Grouped: every series' last bar rises from the same baseline to a
|
|
139
|
+
# similar height, so the collision cascade squashes the labels
|
|
140
|
+
# together. A legend is the honest treatment.
|
|
141
|
+
return endpoint_labels.model_copy(update={"visible": False})
|
|
142
|
+
if normalized.multiples is not None:
|
|
143
|
+
# Each small multiple is its own panel; the rail has no single panel
|
|
144
|
+
# to sit beside, and FacetFeature refuses the composition outright.
|
|
145
|
+
return endpoint_labels.model_copy(update={"visible": False})
|
|
146
|
+
y_fields = (
|
|
147
|
+
(normalized.y,)
|
|
148
|
+
if isinstance(normalized.y, str)
|
|
149
|
+
else tuple(normalized.y) if isinstance(normalized.y, list) else ()
|
|
150
|
+
)
|
|
151
|
+
# A negative (zero-crossing) measure breaks the cumulative-midpoint
|
|
152
|
+
# computation; the render layer raises rather than mislabel. Stack mode
|
|
153
|
+
# zero/normalize/center all reach that raise, so this check runs
|
|
154
|
+
# regardless of which one resolved_stack picked.
|
|
155
|
+
if any(v < 0 for v in _numeric_y_values(data, y_fields)):
|
|
156
|
+
return endpoint_labels.model_copy(update={"visible": False})
|
|
157
|
+
# author_opted_in is False by construction here — the opt-in returned above.
|
|
158
|
+
endpoint_labels = _endpoint_labels_off_for_layers(
|
|
159
|
+
endpoint_labels, normalized, author_opted_in
|
|
160
|
+
)
|
|
161
|
+
if not endpoint_labels.visible:
|
|
162
|
+
return endpoint_labels
|
|
163
|
+
if normalized.sort is not None and not numeric_column_values(
|
|
164
|
+
data, normalized.sort.by
|
|
165
|
+
):
|
|
166
|
+
# Both rails reproduce Vega-Lite's domain order by totalling the sort
|
|
167
|
+
# column per category. On a non-numeric column VL concatenates the
|
|
168
|
+
# strings instead, an order this cannot mirror — so the rail would
|
|
169
|
+
# anchor on a row VL does not draw on top.
|
|
170
|
+
return endpoint_labels.model_copy(update={"visible": False})
|
|
171
|
+
if horizontal and stack_mode == "center":
|
|
172
|
+
# The horizontal rail anchors on the cumulative (0..Σ) axis, which the
|
|
173
|
+
# diverging center-stack domain doesn't have.
|
|
174
|
+
return endpoint_labels.model_copy(update={"visible": False})
|
|
175
|
+
if not _every_series_reaches_the_last_column(
|
|
176
|
+
normalized, data, y_fields, horizontal
|
|
177
|
+
):
|
|
178
|
+
return endpoint_labels.model_copy(update={"visible": False})
|
|
179
|
+
return endpoint_labels
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _every_series_reaches_the_last_column(
|
|
183
|
+
normalized: BarChart,
|
|
184
|
+
data: ChartRows,
|
|
185
|
+
y_fields: tuple[str, ...],
|
|
186
|
+
horizontal: bool,
|
|
187
|
+
) -> bool:
|
|
188
|
+
"""True when the final x column carries a segment for every series.
|
|
189
|
+
|
|
190
|
+
A stacked bar's labels anchor to that column's segment midpoints, so a
|
|
191
|
+
series absent from it has nothing to anchor to and goes unnamed while its
|
|
192
|
+
colour stays in the chart — the reader counts more colours than names.
|
|
193
|
+
Two shapes hit this: a trailing null, and ``color:`` naming the same field
|
|
194
|
+
as ``x:``, which gives every series a single distinct column.
|
|
195
|
+
"""
|
|
196
|
+
color = normalized.color
|
|
197
|
+
x = normalized.x
|
|
198
|
+
if not isinstance(color, str) or not isinstance(x, str):
|
|
199
|
+
return True
|
|
200
|
+
# Each orientation anchors its rail on a different end of the rendered x
|
|
201
|
+
# domain, so ask the same question the rail will: the vertical midpoint
|
|
202
|
+
# path takes the last column (_stacked_midpoints), the horizontal rail the
|
|
203
|
+
# first row (_cumulative_midpoints), both over the order stacked_x_domain_order
|
|
204
|
+
# computes in render/chart/features/endpoint_labels.py — which an authored
|
|
205
|
+
# `sort:` moves. A wholly null anchor row still owns the rail.
|
|
206
|
+
sort = normalized.sort
|
|
207
|
+
domain = stacked_x_domain_order(
|
|
208
|
+
data, x, sort.by if sort else "", bool(sort and sort.order == "desc")
|
|
209
|
+
)
|
|
210
|
+
if not domain:
|
|
211
|
+
return True
|
|
212
|
+
last_x = domain[0] if horizontal else domain[-1]
|
|
213
|
+
every_series: set[str] = set()
|
|
214
|
+
drawn_at_last: set[str] = set()
|
|
215
|
+
for row in data:
|
|
216
|
+
series = row.get(color)
|
|
217
|
+
if series is None:
|
|
218
|
+
continue
|
|
219
|
+
every_series.add(series)
|
|
220
|
+
if row.get(x) == last_x and any(
|
|
221
|
+
row.get(field) is not None for field in y_fields
|
|
222
|
+
):
|
|
223
|
+
drawn_at_last.add(series)
|
|
224
|
+
return drawn_at_last == every_series
|
|
225
|
+
|
|
226
|
+
|
|
96
227
|
def _resolve_bar(
|
|
97
228
|
normalized: BarChart,
|
|
98
229
|
data: ChartRows,
|
|
@@ -106,9 +237,10 @@ def _resolve_bar(
|
|
|
106
237
|
board_rcs = board_style.charts
|
|
107
238
|
chart_rcs = build_resolved_style(board_style, normalized)
|
|
108
239
|
bar = chart_rcs.bar
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
240
|
+
# Flat chart.stack overrides style.bar.stack; fall through to the merged bar
|
|
241
|
+
# style (which already applied style.bar.stack → board theme cascade) so that
|
|
242
|
+
# per-chart style overrides are respected even when chart.stack is None.
|
|
243
|
+
resolved_stack = normalized.stack if normalized.stack is not None else bar.stack
|
|
112
244
|
primary = _with_color_tokens(normalized.style, board_style)
|
|
113
245
|
channels = _channels_for(normalized, data)
|
|
114
246
|
x_ch_type = _classify_to_channel_type(normalized.x, data, is_dimension=True)
|
|
@@ -130,7 +262,7 @@ def _resolve_bar(
|
|
|
130
262
|
hint=_bar_y_hint,
|
|
131
263
|
)
|
|
132
264
|
ax_ov = _extract_axis_overrides(primary, normalized.x_label, normalized.y_label)
|
|
133
|
-
ax_merged, ay_merged = _bake_cartesian_axes(
|
|
265
|
+
ax_merged, ay_merged, ax_band_position, ay_band_position = _bake_cartesian_axes(
|
|
134
266
|
chart_rcs,
|
|
135
267
|
normalized,
|
|
136
268
|
"bar",
|
|
@@ -146,56 +278,83 @@ def _resolve_bar(
|
|
|
146
278
|
# both for the categorical axis's edge below and the chart's own
|
|
147
279
|
# resolved `orientation` field at the bottom of this function.
|
|
148
280
|
orientation = _bar_orientation(normalized, data, ax_merged.time_unit is not None)
|
|
149
|
-
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
281
|
+
# Resolved after orientation: two of the disqualifiers below are specific to
|
|
282
|
+
# the horizontal rail, and the last-column check anchors differently per
|
|
283
|
+
# orientation.
|
|
284
|
+
endpoint_labels, tiny_top_legend = _series_label_layout_for_width(
|
|
285
|
+
_bar_endpoint_labels_for_stack(
|
|
286
|
+
bar.endpoint_labels,
|
|
287
|
+
normalized,
|
|
288
|
+
data,
|
|
289
|
+
resolved_stack,
|
|
290
|
+
orientation == "horizontal",
|
|
291
|
+
_author_asked_for_endpoint_labels(normalized.style),
|
|
292
|
+
),
|
|
293
|
+
width,
|
|
158
294
|
)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
#
|
|
175
|
-
#
|
|
295
|
+
if orientation == "horizontal":
|
|
296
|
+
# For horizontal bar, ay (the semantic measure axis) maps to VL x,
|
|
297
|
+
# which has no left/right orient — _emit_horizontal pops it entirely.
|
|
298
|
+
# ay.position's only remaining role is supplying the swapped
|
|
299
|
+
# categorical axis's (VL y) orient in emitters/bar.py. The deleted
|
|
300
|
+
# categorical_orient field always defaulted "left" there, statically —
|
|
301
|
+
# the horizontal rail sits above the plot (top-row), never left/right,
|
|
302
|
+
# so it never competes for this axis's side the way the vertical
|
|
303
|
+
# endpoint-label rail does. Preserve the static default via
|
|
304
|
+
# _bake_ay_position_left rather than _bake_ay_orient's
|
|
305
|
+
# endpoint-label-aware auto-resolution.
|
|
306
|
+
ay_merged = _bake_ay_position_left(ay_merged)
|
|
307
|
+
else:
|
|
308
|
+
ay_merged = _bake_ay_orient(ay_merged, channels, endpoint_labels.visible)
|
|
309
|
+
# Bar's categorical axis (axis_x) only has a left/right edge when the
|
|
310
|
+
# chart is horizontal — it then renders on VL's y channel at ay.position
|
|
311
|
+
# (see emitters/bar.py's own-orient swap; categorical_orient was deleted
|
|
312
|
+
# 2026-08 — position now serves this role too). A vertical bar's x stays
|
|
313
|
+
# bottom-orient — no edge.
|
|
314
|
+
x_edge = _edge_or_none(ay_merged.position) if orientation == "horizontal" else None
|
|
315
|
+
# Propagate bar.scale.continuous.zero (theme→face cascade result) to ay.scale
|
|
316
|
+
# so the emitter's ay_scale_zero check reflects an authored explicit zero override.
|
|
176
317
|
# Only concrete bools are propagated; "auto"/None means run the picker default
|
|
177
318
|
# (non-stacked bars always anchor at zero by default).
|
|
178
319
|
_bar_scale = bar.scale
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
320
|
+
_bar_zero = (
|
|
321
|
+
_bar_scale.continuous.zero
|
|
322
|
+
if _bar_scale is not None and _bar_scale.continuous is not None
|
|
323
|
+
else None
|
|
324
|
+
)
|
|
325
|
+
if isinstance(_bar_zero, bool):
|
|
326
|
+
existing_ay_scale = ay_merged.scale
|
|
327
|
+
if existing_ay_scale is not None:
|
|
328
|
+
old_cont = existing_ay_scale.continuous
|
|
329
|
+
new_cont = (
|
|
330
|
+
old_cont.model_copy(update={"zero": _bar_zero})
|
|
331
|
+
if old_cont is not None
|
|
332
|
+
else ScaleContinuousStyle(zero=_bar_zero)
|
|
333
|
+
)
|
|
334
|
+
ay_merged = ay_merged.model_copy(
|
|
335
|
+
update={
|
|
336
|
+
"scale": existing_ay_scale.model_copy(
|
|
337
|
+
update={"continuous": new_cont}
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
)
|
|
341
|
+
else:
|
|
342
|
+
ay_merged = ay_merged.model_copy(
|
|
343
|
+
update={
|
|
344
|
+
"scale": BaseScaleStyle(
|
|
345
|
+
continuous=ScaleContinuousStyle(zero=_bar_zero)
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
)
|
|
349
|
+
_ay_cont_log = ay_merged.scale.continuous if ay_merged.scale is not None else None
|
|
350
|
+
if _ay_cont_log is not None and _ay_cont_log.type == "log":
|
|
189
351
|
raise CompilationError.from_code(
|
|
190
352
|
ERR_BAR_LOG_SCALE_NOT_SUPPORTED,
|
|
191
353
|
chart_id=normalized.id,
|
|
192
354
|
)
|
|
193
|
-
# Flat chart.stack overrides style.bar.stack; fall through to the merged bar
|
|
194
|
-
# style (which already applied style.bar.stack → board theme cascade) so that
|
|
195
|
-
# per-chart style overrides are respected even when chart.stack is None.
|
|
196
|
-
resolved_stack = normalized.stack if normalized.stack is not None else bar.stack
|
|
197
355
|
is_stacked = resolved_stack not in (None, "none")
|
|
198
|
-
|
|
356
|
+
_ay_cont_zero = ay_merged.scale.continuous if ay_merged.scale is not None else None
|
|
357
|
+
ay_scale_zero = _ay_cont_zero.zero if _ay_cont_zero is not None else None
|
|
199
358
|
# Non-stacked bars anchor at zero by default unless scale.zero is explicitly False.
|
|
200
359
|
# Stacked bars only anchor when scale.zero is explicitly True.
|
|
201
360
|
bar_zero = (not is_stacked and ay_scale_zero is not False) or ay_scale_zero is True
|
|
@@ -209,7 +368,7 @@ def _resolve_bar(
|
|
|
209
368
|
stable_extent = _eligible_stable_extent(
|
|
210
369
|
ay_merged, stable_y_domain, stacked=is_stacked
|
|
211
370
|
)
|
|
212
|
-
authored_y_domain =
|
|
371
|
+
authored_y_domain = _ay_cont_zero.domain if _ay_cont_zero is not None else None
|
|
213
372
|
if resolved_stack == "normalize" or not y_fields:
|
|
214
373
|
# normalize-stack domain is always [0,1]; no y_field means no ticks.
|
|
215
374
|
tick_values: tuple[float, ...] = ()
|
|
@@ -258,15 +417,16 @@ def _resolve_bar(
|
|
|
258
417
|
stacked_domain_max = apply_headroom(
|
|
259
418
|
_raw_stacked_max, _axis_headroom(ay_merged)
|
|
260
419
|
)
|
|
261
|
-
ax = build_resolved_axis(ax_merged, edge=x_edge)
|
|
420
|
+
ax = build_resolved_axis(ax_merged, band_position=ax_band_position, edge=x_edge)
|
|
262
421
|
ay = build_resolved_axis(
|
|
263
422
|
ay_merged,
|
|
423
|
+
band_position=ay_band_position,
|
|
264
424
|
edge=_edge_or_none(ay_merged.position),
|
|
265
425
|
tick_values=tick_values,
|
|
266
426
|
domain_max=bar_domain_max if bar_domain_max is not None else ...,
|
|
267
427
|
domain_min=bar_domain_min if bar_domain_min is not None else ...,
|
|
268
428
|
)
|
|
269
|
-
style_tail = _cartesian_style_tail(board_rcs,
|
|
429
|
+
style_tail = _cartesian_style_tail(board_rcs, ax, ay)
|
|
270
430
|
_bar_fmt = _measure_tooltip_format(normalized, primary, board_rcs)
|
|
271
431
|
if _bar_fmt:
|
|
272
432
|
style_tail["tooltip_format"] = _bar_fmt
|
|
@@ -285,10 +445,18 @@ def _resolve_bar(
|
|
|
285
445
|
_effective_palette(board_rcs, primary),
|
|
286
446
|
requested_alias_palette=_effective_requested_alias_palette(board_rcs, primary),
|
|
287
447
|
automatic_link_candidate=automatic_link_candidate,
|
|
448
|
+
layout_padding=bar.padding,
|
|
288
449
|
suppress_legend=_suppress_legend_for_endpoint_labels(
|
|
289
|
-
channels, endpoint_labels.visible
|
|
450
|
+
channels, endpoint_labels.visible, bool(normalized.layers)
|
|
451
|
+
),
|
|
452
|
+
# A grouped bar keeps its legend (see _bar_endpoint_labels_for_stack)
|
|
453
|
+
# and reads left to right, so the legend belongs above the plot in the
|
|
454
|
+
# same order as the bars within a group — not down a side rail.
|
|
455
|
+
top_legend=(
|
|
456
|
+
"off"
|
|
457
|
+
if _author_hid_legend(primary)
|
|
458
|
+
else "compact" if tiny_top_legend else ("row" if not is_stacked else "off")
|
|
290
459
|
),
|
|
291
|
-
show_top_legend=show_top_legend,
|
|
292
460
|
)
|
|
293
461
|
resolved_layers = _resolve_layer_list(
|
|
294
462
|
normalized.layers, board_style, "bar", bar, normalized.query_name, 0.0
|
|
@@ -299,19 +467,19 @@ def _resolve_bar(
|
|
|
299
467
|
# label: if either format is None, inherit the resolved measure-axis format.
|
|
300
468
|
raw_total = bar.marks.bar.total_label
|
|
301
469
|
resolved_total = (
|
|
302
|
-
raw_total.model_copy(update={"format": ay.format})
|
|
303
|
-
if raw_total.format is None and ay.format is not None
|
|
470
|
+
raw_total.model_copy(update={"format": ay.labels.format})
|
|
471
|
+
if raw_total.format is None and ay.labels.format is not None
|
|
304
472
|
else raw_total
|
|
305
473
|
)
|
|
306
474
|
bar_mark = bar.marks.bar.model_copy(
|
|
307
475
|
update={
|
|
308
|
-
"labels": _label_format_fallback(bar.marks.bar.labels, ay.format),
|
|
476
|
+
"labels": _label_format_fallback(bar.marks.bar.labels, ay.labels.format),
|
|
309
477
|
"total_label": resolved_total,
|
|
310
478
|
}
|
|
311
479
|
)
|
|
312
480
|
return ResolvedBarChart(
|
|
313
481
|
**bkw,
|
|
314
|
-
**_cartesian_kwargs(normalized,
|
|
482
|
+
**_cartesian_kwargs(normalized, chart_rcs, variables),
|
|
315
483
|
chart_type="bar",
|
|
316
484
|
stack=resolved_stack,
|
|
317
485
|
stacked_domain_max=stacked_domain_max,
|
|
@@ -351,22 +519,28 @@ def _resolve_histogram(
|
|
|
351
519
|
histogram.marks.bar.border apply correctly.
|
|
352
520
|
"""
|
|
353
521
|
board_rcs = board_style.charts
|
|
522
|
+
chart_rcs = build_resolved_style(board_style, normalized)
|
|
354
523
|
primary = _with_color_tokens(normalized.style, board_style)
|
|
355
524
|
hist = merge_onto_base(board_rcs.histogram, primary)
|
|
356
525
|
channels = _channels_for(normalized, data)
|
|
357
526
|
x_ch_type = _classify_to_channel_type(normalized.x, data, is_dimension=True)
|
|
358
527
|
ax_ov = _extract_axis_overrides(primary, normalized.x_label, normalized.y_label)
|
|
359
|
-
# board_rcs (not chart_rcs) suffices
|
|
360
|
-
#
|
|
361
|
-
#
|
|
362
|
-
|
|
528
|
+
# board_rcs (not chart_rcs) suffices for axis baking: axis Layers 4-6
|
|
529
|
+
# (chart-local overrides) reach the cascade via ax_ov regardless of which
|
|
530
|
+
# rcs backs Layers 1-3. chart_rcs is only needed below for the shared
|
|
531
|
+
# background/title-case kwargs, which are genuinely chart-local decisions.
|
|
532
|
+
ax_merged, ay_merged, ax_band_position, ay_band_position = _bake_cartesian_axes(
|
|
363
533
|
board_rcs, normalized, "histogram", x_ch_type, "quantitative", ax_ov
|
|
364
534
|
)
|
|
365
535
|
ay_merged = _bake_ay_position_left(ay_merged)
|
|
366
536
|
# Histogram's x is always a bottom-orient bin axis — no left/right edge.
|
|
367
|
-
ax = build_resolved_axis(ax_merged, edge=None)
|
|
368
|
-
ay = build_resolved_axis(
|
|
369
|
-
|
|
537
|
+
ax = build_resolved_axis(ax_merged, band_position=ax_band_position, edge=None)
|
|
538
|
+
ay = build_resolved_axis(
|
|
539
|
+
ay_merged,
|
|
540
|
+
band_position=ay_band_position,
|
|
541
|
+
edge=_edge_or_none(ay_merged.position),
|
|
542
|
+
)
|
|
543
|
+
style_tail = _cartesian_style_tail(board_rcs, ax, ay)
|
|
370
544
|
# histogram reuses ResolvedBarStyle; these bar-only fields are unread by
|
|
371
545
|
# the histogram emit path (no stack, no endpoint labels, no grouped x offset).
|
|
372
546
|
_s = normalized.style
|
|
@@ -387,8 +561,9 @@ def _resolve_histogram(
|
|
|
387
561
|
board_rcs, primary
|
|
388
562
|
),
|
|
389
563
|
automatic_link_candidate=automatic_link_candidate,
|
|
564
|
+
layout_padding=hist.padding,
|
|
390
565
|
),
|
|
391
|
-
**_cartesian_kwargs(normalized,
|
|
566
|
+
**_cartesian_kwargs(normalized, chart_rcs, variables),
|
|
392
567
|
chart_type="histogram",
|
|
393
568
|
stack="none",
|
|
394
569
|
stacked_domain_max=None,
|
|
@@ -404,7 +579,13 @@ def _resolve_histogram(
|
|
|
404
579
|
rhythm_slot=normalized.rhythm_slot,
|
|
405
580
|
has_layers=bool(normalized.layers),
|
|
406
581
|
),
|
|
407
|
-
|
|
582
|
+
# A histogram bins x and aggregates y to a count, so there is no
|
|
583
|
+
# per-row value for a rail to anchor to — it would place labels
|
|
584
|
+
# against a measure the chart never plots. Bar's slot supplies the
|
|
585
|
+
# rest of the style; this one field is off for the family.
|
|
586
|
+
endpoint_labels=board_rcs.bar.endpoint_labels.model_copy(
|
|
587
|
+
update={"visible": False}
|
|
588
|
+
),
|
|
408
589
|
label_usable_ratio=board_rcs.label_usable_ratio,
|
|
409
590
|
title_font=_tf,
|
|
410
591
|
**style_tail,
|