tree-annotated-plot 0.3.0__tar.gz → 0.4.0__tar.gz
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.
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/CHANGELOG.md +27 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/PKG-INFO +3 -3
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/pyproject.toml +9 -2
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/src/tree_annotated_plot/_config.py +12 -2
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/src/tree_annotated_plot/_plot.py +36 -14
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/tests/test_appearance_tuning.py +27 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/tests/test_cli.py +1 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/tests/test_label_connection.py +48 -3
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/tests/test_tree_location.py +52 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/.github/workflows/docs.yml +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/.github/workflows/release.yml +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/.github/workflows/tests.yml +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/.gitignore +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/CLAUDE.md +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/LICENSE +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/README.md +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/docs/cli.md +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/docs/examples.md +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/docs/index.md +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/docs/python-api.md +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/examples/fetch_auspice_data.py +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/examples/flu-seqneut-2025to2026_titer_charts.py +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/examples/synthetic_example.py +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/mkdocs.yml +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/scripts/build_docs.sh +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/scripts/check.sh +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/scripts/generate_docs_assets.py +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/src/tree_annotated_plot/__init__.py +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/src/tree_annotated_plot/_color.py +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/src/tree_annotated_plot/_tree.py +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/src/tree_annotated_plot/cli.py +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/tests/test_branch_length.py +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/tests/test_chart_loader.py +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/tests/test_color_tree.py +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/tests/test_examples_run.py +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/tests/test_introspection.py +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/tests/test_line_order.py +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/tests/test_plot.py +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/tests/test_plot_config.py +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/tests/test_real_data.py +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/tests/test_reconciliation.py +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/tests/test_scale_bar.py +0 -0
- {tree_annotated_plot-0.3.0 → tree_annotated_plot-0.4.0}/tests/test_style.py +0 -0
|
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-08-20
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `spacing` (default `0`): pixels of blank space between the tree panel and
|
|
15
|
+
the chart panel. `0` keeps the two butted together, as before. Distinct from
|
|
16
|
+
`shift_tree_loc`, which moves the tree within its own panel by resizing the
|
|
17
|
+
label strip. CLI form: `--spacing`.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Pin `ruff` to `>=0.16,<0.17` and state the lint rules explicitly with
|
|
22
|
+
`[tool.ruff.lint] select`. Ruff's *default* rule set widened in 0.16, which
|
|
23
|
+
turned CI red on unchanged code; the selection is now a project decision
|
|
24
|
+
rather than whatever the installed ruff defaults to. Development-only, no
|
|
25
|
+
effect on the package.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- With `connect_leader_to_label` on, strain labels were anchored exactly on the
|
|
30
|
+
tree panel's chart-facing edge, so the text ran up against the chart's frame
|
|
31
|
+
and the frame line became hard to read. The pad that the label strip already
|
|
32
|
+
reserved is now applied between the text and the chart, and the leader lines
|
|
33
|
+
stop where the text begins rather than continuing to the panel edge. The gap
|
|
34
|
+
is `max(3, strain_label_font_size * 0.2)` px, and the tree panel grows by that
|
|
35
|
+
much, so a hand-tuned `shift_tree_loc` may want the same adjustment.
|
|
36
|
+
|
|
10
37
|
## [0.3.0] - 2026-05-19
|
|
11
38
|
|
|
12
39
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: tree-annotated-plot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Annotate the axis of an Altair / Vega-Lite plot with a phylogenetic tree.
|
|
5
5
|
Project-URL: Documentation, https://jbloomlab.github.io/tree-annotated-plot/
|
|
6
6
|
Project-URL: Homepage, https://github.com/jbloomlab/tree-annotated-plot
|
|
@@ -43,7 +43,7 @@ Requires-Dist: vl-convert-python>=1.9
|
|
|
43
43
|
Provides-Extra: dev
|
|
44
44
|
Requires-Dist: black>=24; extra == 'dev'
|
|
45
45
|
Requires-Dist: pytest>=8; extra == 'dev'
|
|
46
|
-
Requires-Dist: ruff
|
|
46
|
+
Requires-Dist: ruff<0.17,>=0.16; extra == 'dev'
|
|
47
47
|
Provides-Extra: docs
|
|
48
48
|
Requires-Dist: mkdocs-click>=0.8; extra == 'docs'
|
|
49
49
|
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tree-annotated-plot"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Annotate the axis of an Altair / Vega-Lite plot with a phylogenetic tree."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -36,7 +36,7 @@ Issues = "https://github.com/jbloomlab/tree-annotated-plot/issues"
|
|
|
36
36
|
[project.optional-dependencies]
|
|
37
37
|
dev = [
|
|
38
38
|
"pytest>=8",
|
|
39
|
-
"ruff>=0.
|
|
39
|
+
"ruff>=0.16,<0.17",
|
|
40
40
|
"black>=24",
|
|
41
41
|
]
|
|
42
42
|
docs = [
|
|
@@ -58,3 +58,10 @@ target-version = ["py311"]
|
|
|
58
58
|
|
|
59
59
|
[tool.ruff]
|
|
60
60
|
target-version = "py311"
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint]
|
|
63
|
+
# Ruff's default rule set is not stable across releases -- 0.16 broadened it,
|
|
64
|
+
# which turned CI red on unchanged code. State the rules we lint against so a
|
|
65
|
+
# ruff upgrade changes them only when we say so. These are ruff's pre-0.16
|
|
66
|
+
# defaults, which is what this project has been checked against all along.
|
|
67
|
+
select = ["E4", "E7", "E9", "F"]
|
|
@@ -68,6 +68,15 @@ class PlotConfig:
|
|
|
68
68
|
"(x-encoded).",
|
|
69
69
|
] = None
|
|
70
70
|
|
|
71
|
+
spacing: Annotated[
|
|
72
|
+
int,
|
|
73
|
+
"Pixels of blank space between the tree panel and the chart panel. "
|
|
74
|
+
"Default 0, which butts the two together. Distinct from "
|
|
75
|
+
"`shift_tree_loc`, which moves the tree within its own panel by "
|
|
76
|
+
"resizing the label strip; `spacing` sets the gap between the panels "
|
|
77
|
+
"and applies whether or not `connect_leader_to_label` is on.",
|
|
78
|
+
] = 0
|
|
79
|
+
|
|
71
80
|
tree_line_width: Annotated[
|
|
72
81
|
float,
|
|
73
82
|
"Stroke width (px) for the tree's branch lines. Default 2.",
|
|
@@ -155,8 +164,9 @@ class PlotConfig:
|
|
|
155
164
|
shift_tree_loc: Annotated[
|
|
156
165
|
int,
|
|
157
166
|
"Pixels by which to shift the tree toward (positive) or away from "
|
|
158
|
-
"(negative) the chart
|
|
159
|
-
"connect_leader_to_label is off
|
|
167
|
+
"(negative) the chart, by resizing the label strip inside the tree "
|
|
168
|
+
"panel. Default 0. Has no effect when connect_leader_to_label is off; "
|
|
169
|
+
"to set the gap between the tree and chart panels use `spacing`.",
|
|
160
170
|
] = 0
|
|
161
171
|
|
|
162
172
|
color_tree_by: Annotated[
|
|
@@ -49,6 +49,7 @@ def plot(
|
|
|
49
49
|
branch_length: Literal["div", "num_date"],
|
|
50
50
|
tree_size: int = 100,
|
|
51
51
|
tree_location: TreeLocation | None = None,
|
|
52
|
+
spacing: int = 0,
|
|
52
53
|
tree_line_width: float = 2.0,
|
|
53
54
|
tree_node_size: float = 45,
|
|
54
55
|
leader_line_width: float = 1.0,
|
|
@@ -77,6 +78,7 @@ def plot(
|
|
|
77
78
|
branch_length=branch_length,
|
|
78
79
|
tree_size=tree_size,
|
|
79
80
|
tree_location=tree_location,
|
|
81
|
+
spacing=spacing,
|
|
80
82
|
tree_line_width=tree_line_width,
|
|
81
83
|
tree_node_size=tree_node_size,
|
|
82
84
|
leader_line_width=leader_line_width,
|
|
@@ -281,7 +283,10 @@ def _build(
|
|
|
281
283
|
hoisted_config, hoisted_other = _pop_toplevel_only_attrs(new_chart)
|
|
282
284
|
|
|
283
285
|
combined = _concat_for_location(
|
|
284
|
-
tree_chart=tree_chart,
|
|
286
|
+
tree_chart=tree_chart,
|
|
287
|
+
user_chart=new_chart,
|
|
288
|
+
location=location,
|
|
289
|
+
spacing=config.spacing,
|
|
285
290
|
)
|
|
286
291
|
_apply_combined_config(combined, hoisted_config)
|
|
287
292
|
for k, v in hoisted_other.items():
|
|
@@ -320,6 +325,7 @@ def _concat_for_location(
|
|
|
320
325
|
tree_chart: alt.TopLevelMixin,
|
|
321
326
|
user_chart: alt.TopLevelMixin,
|
|
322
327
|
location: TreeLocation,
|
|
328
|
+
spacing: int,
|
|
323
329
|
) -> alt.HConcatChart | alt.VConcatChart:
|
|
324
330
|
"""Concat tree and chart in the order implied by the tree's location.
|
|
325
331
|
|
|
@@ -333,19 +339,19 @@ def _concat_for_location(
|
|
|
333
339
|
user-chart marks whose color values aren't in the tree's domain.
|
|
334
340
|
"""
|
|
335
341
|
if location == "left":
|
|
336
|
-
return alt.hconcat(tree_chart, user_chart, spacing=
|
|
342
|
+
return alt.hconcat(tree_chart, user_chart, spacing=spacing).resolve_scale(
|
|
337
343
|
y="independent", color="independent"
|
|
338
344
|
)
|
|
339
345
|
if location == "right":
|
|
340
|
-
return alt.hconcat(user_chart, tree_chart, spacing=
|
|
346
|
+
return alt.hconcat(user_chart, tree_chart, spacing=spacing).resolve_scale(
|
|
341
347
|
y="independent", color="independent"
|
|
342
348
|
)
|
|
343
349
|
if location == "top":
|
|
344
|
-
return alt.vconcat(tree_chart, user_chart, spacing=
|
|
350
|
+
return alt.vconcat(tree_chart, user_chart, spacing=spacing).resolve_scale(
|
|
345
351
|
x="independent", color="independent"
|
|
346
352
|
)
|
|
347
353
|
if location == "bottom":
|
|
348
|
-
return alt.vconcat(user_chart, tree_chart, spacing=
|
|
354
|
+
return alt.vconcat(user_chart, tree_chart, spacing=spacing).resolve_scale(
|
|
349
355
|
x="independent", color="independent"
|
|
350
356
|
)
|
|
351
357
|
raise ValueError(f"unreachable: tree_location={location!r}")
|
|
@@ -1475,6 +1481,11 @@ def _build_scale_bar_layer(
|
|
|
1475
1481
|
|
|
1476
1482
|
_LABEL_PAD_PX_MIN = 4
|
|
1477
1483
|
_LABEL_PAD_RATIO = 0.4 # `LABEL_PAD_PX = max(MIN, font_size * RATIO)`
|
|
1484
|
+
# Gap between the labels and the chart, kept separate from the tree-facing pad
|
|
1485
|
+
# above because it clears the chart's frame rather than the deepest tip, and so
|
|
1486
|
+
# wants a small fixed clearance rather than the pad's larger, faster-growing one.
|
|
1487
|
+
_LABEL_CHART_GAP_PX_MIN = 3
|
|
1488
|
+
_LABEL_CHART_GAP_RATIO = 0.2 # `LABEL_CHART_GAP_PX = max(MIN, font_size * RATIO)`
|
|
1478
1489
|
_LABEL_CHAR_PX_RATIO = 0.6 # rough proportional sans-serif glyph-width estimate
|
|
1479
1490
|
_LABEL_HALO_RATIO = 0.6 # white-halo strokeWidth as a fraction of font_size
|
|
1480
1491
|
|
|
@@ -1614,9 +1625,15 @@ def _build_tree_chart(
|
|
|
1614
1625
|
max_name_len = max((len(n) for n in names), default=0)
|
|
1615
1626
|
char_px = strain_label_font_size * _LABEL_CHAR_PX_RATIO
|
|
1616
1627
|
label_pad_px = max(_LABEL_PAD_PX_MIN, strain_label_font_size * _LABEL_PAD_RATIO)
|
|
1628
|
+
chart_gap_px = max(
|
|
1629
|
+
_LABEL_CHART_GAP_PX_MIN, strain_label_font_size * _LABEL_CHART_GAP_RATIO
|
|
1630
|
+
)
|
|
1617
1631
|
# Strip needs to fit the longest label plus `halo_px / 2` of halo
|
|
1618
|
-
# extension on the leader-facing side,
|
|
1619
|
-
|
|
1632
|
+
# extension on the leader-facing side, a pad between the tree and the
|
|
1633
|
+
# text, and a smaller gap between the text and the chart.
|
|
1634
|
+
label_pixel_width = (
|
|
1635
|
+
label_pad_px + chart_gap_px + max_name_len * char_px + halo_px / 2
|
|
1636
|
+
)
|
|
1620
1637
|
strip_pixel_width = label_pixel_width - shift_tree_loc
|
|
1621
1638
|
if strip_pixel_width <= 0:
|
|
1622
1639
|
raise ValueError(
|
|
@@ -1629,9 +1646,13 @@ def _build_tree_chart(
|
|
|
1629
1646
|
per_pixel = branch_span / tree_size if tree_size else 0.0
|
|
1630
1647
|
extra_branch_units = strip_pixel_width * per_pixel
|
|
1631
1648
|
chart_edge_branch = branch_max + extra_branch_units
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1649
|
+
# Anchor the labels `chart_gap_px` *inside* the chart-facing edge rather
|
|
1650
|
+
# than on it, so the text does not run up against the chart's frame. The
|
|
1651
|
+
# leaders stop at the same place, where the text begins.
|
|
1652
|
+
label_anchor_branch = chart_edge_branch - chart_gap_px * per_pixel
|
|
1653
|
+
tips_df = tips_df.assign(x_label=label_anchor_branch)
|
|
1654
|
+
leader_df = tips_df[tips_df["x"] < label_anchor_branch].assign(
|
|
1655
|
+
x2=label_anchor_branch
|
|
1635
1656
|
)
|
|
1636
1657
|
extended_tree_size = tree_size + strip_pixel_width
|
|
1637
1658
|
else:
|
|
@@ -1680,9 +1701,9 @@ def _build_tree_chart(
|
|
|
1680
1701
|
# When connect_leader_to_label is on, the branch domain is extended
|
|
1681
1702
|
# past `branch_max` to `chart_edge_branch` so the label strip has
|
|
1682
1703
|
# data-units to occupy; tips at `branch_max` still sit at pixel
|
|
1683
|
-
# `tree_size` on the panel. Each label's chart-facing edge
|
|
1684
|
-
#
|
|
1685
|
-
# tree on the left, left for tree on the right).
|
|
1704
|
+
# `tree_size` on the panel. Each label's chart-facing edge sits
|
|
1705
|
+
# `chart_gap_px` inside `chart_edge_branch`, aligned outward (right
|
|
1706
|
+
# for tree on the left, left for tree on the right).
|
|
1686
1707
|
if tree_location == "left":
|
|
1687
1708
|
branch_domain = [branch_min, chart_edge_branch]
|
|
1688
1709
|
text_align = "right"
|
|
@@ -1783,7 +1804,8 @@ def _build_tree_chart(
|
|
|
1783
1804
|
# strip at bottom, opposite the chart above).
|
|
1784
1805
|
# tree_location="bottom" → root at bottom → branch_max at top (label
|
|
1785
1806
|
# strip at top, opposite the chart below).
|
|
1786
|
-
# Each label's chart-facing edge
|
|
1807
|
+
# Each label's chart-facing edge sits `chart_gap_px` inside
|
|
1808
|
+
# `chart_edge_branch`.
|
|
1787
1809
|
# The text mark is rotated 270° (reads bottom-to-top), which maps
|
|
1788
1810
|
# pre-rotation `align="right"` to a top anchor (text extends down)
|
|
1789
1811
|
# and `align="left"` to a bottom anchor (text extends up). For tree
|
|
@@ -795,3 +795,30 @@ def test_cli_scale_bar_font_size(tmp_path):
|
|
|
795
795
|
collect_text_marks(tree_panel, text_marks)
|
|
796
796
|
sizes = [m.get("fontSize") for m in text_marks if "fontSize" in m]
|
|
797
797
|
assert 16.0 in sizes or 16 in sizes
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def test_cli_spacing(tmp_path):
|
|
801
|
+
"""`--spacing` reaches the saved spec's top-level concat gap."""
|
|
802
|
+
tree_path, chart_path, out_path = _cli_setup(
|
|
803
|
+
tmp_path, _attr_auspice(), _vertical_chart(["A", "B", "C", "D"])
|
|
804
|
+
)
|
|
805
|
+
_run_cli(
|
|
806
|
+
[
|
|
807
|
+
"--tree",
|
|
808
|
+
str(tree_path),
|
|
809
|
+
"--chart",
|
|
810
|
+
str(chart_path),
|
|
811
|
+
"--output",
|
|
812
|
+
str(out_path),
|
|
813
|
+
"--chart-strain-field",
|
|
814
|
+
"strain",
|
|
815
|
+
"--tree-strain-field",
|
|
816
|
+
"name",
|
|
817
|
+
"--branch-length",
|
|
818
|
+
"div",
|
|
819
|
+
"--spacing",
|
|
820
|
+
"9",
|
|
821
|
+
]
|
|
822
|
+
)
|
|
823
|
+
spec = json.loads(out_path.read_text())
|
|
824
|
+
assert spec["spacing"] == 9
|
|
@@ -285,9 +285,9 @@ def test_default_leader_endpoint_stops_at_branch_max() -> None:
|
|
|
285
285
|
|
|
286
286
|
def test_vertical_left_renders_text_with_right_align() -> None:
|
|
287
287
|
"""Vertical layout with `connect_leader_to_label=True` and default
|
|
288
|
-
`tree_location="left"`: tree on left, chart on right; labels
|
|
289
|
-
|
|
290
|
-
|
|
288
|
+
`tree_location="left"`: tree on left, chart on right; labels sit just
|
|
289
|
+
inside the panel's right (chart-facing) edge. Text mark uses
|
|
290
|
+
`align="right"` and `x_label > branch_max`."""
|
|
291
291
|
out = tree_annotated_plot.plot(_auspice(), _vertical_chart(), **_on_kw())
|
|
292
292
|
text = _text_layer(out)
|
|
293
293
|
assert text is not None
|
|
@@ -557,3 +557,48 @@ def test_default_off_keeps_user_axis_labels_intact() -> None:
|
|
|
557
557
|
assert axis.get("labelFontWeight") == "bold"
|
|
558
558
|
assert axis.get("labels") is not False
|
|
559
559
|
assert axis.get("ticks") is not False
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
# ---------- label inset from the chart-facing edge ----------
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def test_labels_sit_inside_the_chart_facing_edge() -> None:
|
|
566
|
+
"""Labels are anchored one pad *inside* `chart_edge_branch` rather than on
|
|
567
|
+
it, so the glyphs do not run up against the chart panel's frame. The panel
|
|
568
|
+
itself still extends to `chart_edge_branch`, which is the chart-facing end
|
|
569
|
+
of the branch scale's domain."""
|
|
570
|
+
out = tree_annotated_plot.plot(_auspice(), _vertical_chart(), **_on_kw())
|
|
571
|
+
text = _text_layer(out)
|
|
572
|
+
assert text is not None
|
|
573
|
+
# tree on the left → branch domain is [branch_min, chart_edge_branch]
|
|
574
|
+
chart_edge_branch = text["encoding"]["x"]["scale"]["domain"][1]
|
|
575
|
+
rows = _resolve_dataset(out.to_dict(), text)
|
|
576
|
+
assert rows
|
|
577
|
+
for row in rows:
|
|
578
|
+
assert row["x_label"] < chart_edge_branch
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def test_labels_sit_inside_the_chart_facing_edge_horizontal() -> None:
|
|
582
|
+
"""Same inset in horizontal layout, where the default tree_location is
|
|
583
|
+
"bottom" and the branch domain runs [branch_min, chart_edge_branch] on y."""
|
|
584
|
+
out = tree_annotated_plot.plot(_auspice(), _horizontal_chart(), **_on_kw())
|
|
585
|
+
text = _text_layer(out)
|
|
586
|
+
assert text is not None
|
|
587
|
+
chart_edge_branch = text["encoding"]["y"]["scale"]["domain"][1]
|
|
588
|
+
rows = _resolve_dataset(out.to_dict(), text)
|
|
589
|
+
assert rows
|
|
590
|
+
for row in rows:
|
|
591
|
+
assert row["x_label"] < chart_edge_branch
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def test_leaders_stop_where_the_labels_begin() -> None:
|
|
595
|
+
"""The leaders run to the label anchor rather than on to the panel edge, so
|
|
596
|
+
the dashed line meets the text instead of passing under it to the frame."""
|
|
597
|
+
out = tree_annotated_plot.plot(_auspice(), _vertical_chart(), **_on_kw())
|
|
598
|
+
text = _text_layer(out)
|
|
599
|
+
assert text is not None
|
|
600
|
+
anchors = {row["x_label"] for row in _resolve_dataset(out.to_dict(), text)}
|
|
601
|
+
assert len(anchors) == 1, f"expected one shared label anchor, got {anchors}"
|
|
602
|
+
(anchor,) = anchors
|
|
603
|
+
for row in _resolve_dataset(out.to_dict(), _leader_layer(out)):
|
|
604
|
+
assert row["x2"] == pytest.approx(anchor)
|
|
@@ -181,3 +181,55 @@ def test_right_with_x_encoded_strain_raises() -> None:
|
|
|
181
181
|
tree_annotated_plot.plot(
|
|
182
182
|
_auspice_4_tip(), _horizontal_chart(), tree_location="right", **_kw()
|
|
183
183
|
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ---------- spacing between the tree and chart panels ----------
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@pytest.mark.parametrize(
|
|
190
|
+
("chart_fn", "location"),
|
|
191
|
+
[
|
|
192
|
+
(_vertical_chart, "left"),
|
|
193
|
+
(_vertical_chart, "right"),
|
|
194
|
+
(_horizontal_chart, "top"),
|
|
195
|
+
(_horizontal_chart, "bottom"),
|
|
196
|
+
],
|
|
197
|
+
)
|
|
198
|
+
def test_spacing_defaults_to_zero(chart_fn, location) -> None:
|
|
199
|
+
"""The default butts the two panels together, as before `spacing` existed."""
|
|
200
|
+
out = tree_annotated_plot.plot(
|
|
201
|
+
_auspice_4_tip(), chart_fn(), tree_location=location, **_kw()
|
|
202
|
+
)
|
|
203
|
+
assert out.to_dict()["spacing"] == 0
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@pytest.mark.parametrize(
|
|
207
|
+
("chart_fn", "location"),
|
|
208
|
+
[
|
|
209
|
+
(_vertical_chart, "left"),
|
|
210
|
+
(_vertical_chart, "right"),
|
|
211
|
+
(_horizontal_chart, "top"),
|
|
212
|
+
(_horizontal_chart, "bottom"),
|
|
213
|
+
],
|
|
214
|
+
)
|
|
215
|
+
def test_spacing_propagates_to_the_concat(chart_fn, location) -> None:
|
|
216
|
+
"""An explicit `spacing` reaches the top-level concat spec, for every
|
|
217
|
+
`tree_location` — the gap is between the two panels, so it applies
|
|
218
|
+
uniformly to the hconcat and vconcat forms alike."""
|
|
219
|
+
out = tree_annotated_plot.plot(
|
|
220
|
+
_auspice_4_tip(), chart_fn(), tree_location=location, spacing=7, **_kw()
|
|
221
|
+
)
|
|
222
|
+
assert out.to_dict()["spacing"] == 7
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_spacing_does_not_disturb_panel_sizes() -> None:
|
|
226
|
+
"""Spacing is applied along the branch axis, between panels, so neither
|
|
227
|
+
panel's own dimensions change and strain-row alignment is untouched."""
|
|
228
|
+
out_zero = tree_annotated_plot.plot(_auspice_4_tip(), _vertical_chart(), **_kw())
|
|
229
|
+
out_gap = tree_annotated_plot.plot(
|
|
230
|
+
_auspice_4_tip(), _vertical_chart(), spacing=12, **_kw()
|
|
231
|
+
)
|
|
232
|
+
d_zero, d_gap = out_zero.to_dict(), out_gap.to_dict()
|
|
233
|
+
for i in (0, 1):
|
|
234
|
+
assert d_zero["hconcat"][i]["width"] == d_gap["hconcat"][i]["width"]
|
|
235
|
+
assert d_zero["hconcat"][i].get("height") == d_gap["hconcat"][i].get("height")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|