tree-annotated-plot 0.0.1__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.
- tree_annotated_plot/__init__.py +11 -0
- tree_annotated_plot/_config.py +215 -0
- tree_annotated_plot/_plot.py +1337 -0
- tree_annotated_plot/_tree.py +307 -0
- tree_annotated_plot/cli.py +165 -0
- tree_annotated_plot-0.0.1.dist-info/METADATA +225 -0
- tree_annotated_plot-0.0.1.dist-info/RECORD +10 -0
- tree_annotated_plot-0.0.1.dist-info/WHEEL +4 -0
- tree_annotated_plot-0.0.1.dist-info/entry_points.txt +2 -0
- tree_annotated_plot-0.0.1.dist-info/licenses/LICENSE +19 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""tree-annotated-plot: plots with a categorical axis aligned to a phylogenetic tree."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
5
|
+
from ._config import PlotConfig
|
|
6
|
+
from ._plot import plot
|
|
7
|
+
from ._tree import TreeNode, load_auspice
|
|
8
|
+
|
|
9
|
+
__version__ = version("tree-annotated-plot")
|
|
10
|
+
|
|
11
|
+
__all__ = ["plot", "PlotConfig", "load_auspice", "TreeNode", "__version__"]
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""`PlotConfig`: the single source of truth for plot-parameter descriptions.
|
|
2
|
+
|
|
3
|
+
Both `tree_annotated_plot.plot` and the CLI consume `PlotConfig`. Each
|
|
4
|
+
field's `Annotated[T, str]` metadata is the canonical description: it
|
|
5
|
+
is pulled into the function's docstring at import time (via
|
|
6
|
+
`_render_numpy_params`, called from `_plot.py`), and into each click
|
|
7
|
+
option's `help` text by `tree_annotated_plot.cli`. Adding or editing a
|
|
8
|
+
parameter description therefore takes one edit, not two.
|
|
9
|
+
|
|
10
|
+
For the rare case where the Python docstring needs more prose than the
|
|
11
|
+
CLI `--help` (e.g. cross-references to Python-only concepts), add an
|
|
12
|
+
entry to `PARAM_DOC_EXTRAS` keyed by the field name; the extra prose
|
|
13
|
+
is appended to that field's description in the docstring only.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import dataclasses
|
|
19
|
+
import textwrap
|
|
20
|
+
import typing
|
|
21
|
+
from typing import Annotated, Literal
|
|
22
|
+
|
|
23
|
+
TreeLocation = Literal["left", "right", "top", "bottom"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
27
|
+
class PlotConfig:
|
|
28
|
+
"""Configuration for `tree_annotated_plot.plot`.
|
|
29
|
+
|
|
30
|
+
Each field's type annotation is `Annotated[T, "<description>"]`. The
|
|
31
|
+
description is the canonical text that appears in the function's
|
|
32
|
+
docstring, the click `--help` text, and the rendered Python API page.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
chart_strain_field: Annotated[
|
|
36
|
+
str,
|
|
37
|
+
"Required. The data-column name the chart's strain axis encodes "
|
|
38
|
+
'(e.g. "strain" or "axis_label").',
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
tree_strain_field: Annotated[
|
|
42
|
+
str,
|
|
43
|
+
"Required. Where on each tree tip to find the strain identifier. "
|
|
44
|
+
'The literal string "name" selects the top-level Auspice node `name` '
|
|
45
|
+
"field; any other value X selects node_attrs[X] (auto-unwrapping the "
|
|
46
|
+
'Auspice {"value": ...} convention). Dotted paths are not accepted.',
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
branch_length: Annotated[
|
|
50
|
+
Literal["div", "num_date"],
|
|
51
|
+
"Required. Which Auspice node attribute supplies branch lengths. "
|
|
52
|
+
'"div" means divergence branch lengths; "num_date" means calendar '
|
|
53
|
+
"dates.",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
tree_size: Annotated[
|
|
57
|
+
int,
|
|
58
|
+
"Size in pixels of tree depth. For vertical layout (chart strain on "
|
|
59
|
+
"`y`) this is the tree panel's *width*; for horizontal layout (chart "
|
|
60
|
+
"strain on `x`) this is the tree panel's *height*.",
|
|
61
|
+
] = 100
|
|
62
|
+
|
|
63
|
+
tree_location: Annotated[
|
|
64
|
+
TreeLocation | None,
|
|
65
|
+
"Which side of the chart to draw the tree on. Defaults to the side "
|
|
66
|
+
'with the strain-axis labels ("left" for y-encoded strain, "bottom" '
|
|
67
|
+
'for x-encoded). Other valid values: "right" (y-encoded), "top" '
|
|
68
|
+
"(x-encoded).",
|
|
69
|
+
] = None
|
|
70
|
+
|
|
71
|
+
tree_line_width: Annotated[
|
|
72
|
+
float,
|
|
73
|
+
"Stroke width (px) for the tree's branch lines. Default 1.5.",
|
|
74
|
+
] = 1.5
|
|
75
|
+
|
|
76
|
+
tree_node_size: Annotated[
|
|
77
|
+
float,
|
|
78
|
+
"Area (px²) of the small filled circles drawn at each tip. "
|
|
79
|
+
"Default 28. Setting tree_node_size=0 disables the tip-circle "
|
|
80
|
+
"layer entirely.",
|
|
81
|
+
] = 28
|
|
82
|
+
|
|
83
|
+
leader_line_width: Annotated[
|
|
84
|
+
float,
|
|
85
|
+
"Stroke width (px) for the dashed leader lines that connect each "
|
|
86
|
+
"tip's branch endpoint to the strain row when the branch doesn't "
|
|
87
|
+
"extend all the way to branch_max. Default 1.0. Setting "
|
|
88
|
+
"leader_line_width=0 disables the leader-line layer entirely.",
|
|
89
|
+
] = 1.0
|
|
90
|
+
|
|
91
|
+
scale_bar: Annotated[
|
|
92
|
+
bool,
|
|
93
|
+
"Off by default. When on, adds a small bar in the tree panel "
|
|
94
|
+
"showing the branch-length scale. Tip-row alignment with the "
|
|
95
|
+
"chart is preserved.",
|
|
96
|
+
] = False
|
|
97
|
+
|
|
98
|
+
branch_length_units: Annotated[
|
|
99
|
+
str | None,
|
|
100
|
+
'Used only when scale_bar is on and branch_length="div": the unit '
|
|
101
|
+
"string pasted after the bar's numeric length (e.g. "
|
|
102
|
+
'"substitutions/site"). None renders unitless. For '
|
|
103
|
+
'branch_length="num_date" the label is always in years/months '
|
|
104
|
+
"and this argument is ignored.",
|
|
105
|
+
] = None
|
|
106
|
+
|
|
107
|
+
prune_tree_to_chart: Annotated[
|
|
108
|
+
bool,
|
|
109
|
+
"When off (default), tree tips not present in the chart's strain "
|
|
110
|
+
"set are a fatal error. When on, those tips (and any internal "
|
|
111
|
+
"nodes whose subtrees become empty) are dropped before drawing, "
|
|
112
|
+
"with single-child internals collapsed into their kept child. "
|
|
113
|
+
"Chart strains not present in the tree are *always* fatal "
|
|
114
|
+
"regardless of this flag — pruning would silently lose plot data.",
|
|
115
|
+
] = False
|
|
116
|
+
|
|
117
|
+
strict_version: Annotated[
|
|
118
|
+
bool,
|
|
119
|
+
"When on (default), known-stale specs raise: Vega-Lite 5 or "
|
|
120
|
+
"earlier, and Auspice JSON whose `version` is not v2. When off, "
|
|
121
|
+
"those become warnings and parsing proceeds.",
|
|
122
|
+
] = True
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# Sidecar for Python-docstring-only prose, keyed by PlotConfig field name.
|
|
126
|
+
# Empty by default — add an entry when a field's docstring entry needs more
|
|
127
|
+
# than the CLI `--help` text covers. The extras are appended to the field's
|
|
128
|
+
# description in `tree_annotated_plot.plot.__doc__`, never in CLI --help.
|
|
129
|
+
PARAM_DOC_EXTRAS: dict[str, str] = {}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# Descriptions for the three data-input parameters. These can't live on
|
|
133
|
+
# PlotConfig because their *types* differ between surfaces (Python accepts
|
|
134
|
+
# live objects; the CLI accepts only file paths), but their descriptions
|
|
135
|
+
# can — and should — be single-sourced. The CLI reads these for its --help
|
|
136
|
+
# text; the Python docstring header in `_plot.py` interpolates them too.
|
|
137
|
+
TREE_DESCRIPTION = (
|
|
138
|
+
"Phylogenetic tree in Auspice JSON v2 format. The CLI accepts a file "
|
|
139
|
+
"path; the Python API additionally accepts a parsed dict or a "
|
|
140
|
+
"pre-built `tree_annotated_plot.TreeNode`."
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
CHART_DESCRIPTION = (
|
|
144
|
+
"Vega-Lite chart whose strain axis the tree will annotate. The CLI "
|
|
145
|
+
"accepts a saved spec on disk — either *.json (canonical) or *.html "
|
|
146
|
+
"(extracted from altair's default save template). The Python API "
|
|
147
|
+
"additionally accepts a live `altair.Chart`-or-subclass object or a "
|
|
148
|
+
"parsed spec dict. Must encode `chart_strain_field` on `x` or `y`."
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
OUTPUT_DESCRIPTION = (
|
|
152
|
+
"Where to save the combined plot. Format inferred from extension: "
|
|
153
|
+
".html, .json, .png, .svg, .pdf."
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def field_description(field_name: str) -> str:
|
|
158
|
+
"""Return the Annotated description of a PlotConfig field."""
|
|
159
|
+
hints = typing.get_type_hints(PlotConfig, include_extras=True)
|
|
160
|
+
annotated = hints[field_name]
|
|
161
|
+
return annotated.__metadata__[0]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _render_data_param(name: str, description: str, width: int = 75) -> str:
|
|
165
|
+
"""Render one NumPy-style parameter block for a non-PlotConfig data input.
|
|
166
|
+
|
|
167
|
+
Used by `_plot.py` to fold the `tree` and `chart` descriptions
|
|
168
|
+
(`TREE_DESCRIPTION`, `CHART_DESCRIPTION`) into `plot.__doc__` in the same
|
|
169
|
+
NumPy shape as the PlotConfig-derived parameters rendered by
|
|
170
|
+
`_render_numpy_params`.
|
|
171
|
+
"""
|
|
172
|
+
body = textwrap.fill(
|
|
173
|
+
description, width=width, initial_indent=" ", subsequent_indent=" "
|
|
174
|
+
)
|
|
175
|
+
return f"{name}\n{body}"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _render_numpy_params(
|
|
179
|
+
extras: dict[str, str] | None = None,
|
|
180
|
+
width: int = 75,
|
|
181
|
+
) -> str:
|
|
182
|
+
"""Render PlotConfig fields as the body of a NumPy `Parameters` block.
|
|
183
|
+
|
|
184
|
+
Returns the per-field entries only — callers prepend the
|
|
185
|
+
`Parameters\\n----------` section header. Each entry is the field
|
|
186
|
+
name on its own line, followed by the field's `Annotated` description
|
|
187
|
+
(and any matching `extras` prose) indented by four spaces and word-wrapped
|
|
188
|
+
to `width`.
|
|
189
|
+
"""
|
|
190
|
+
extras = extras or {}
|
|
191
|
+
hints = typing.get_type_hints(PlotConfig, include_extras=True)
|
|
192
|
+
chunks: list[str] = []
|
|
193
|
+
for field in dataclasses.fields(PlotConfig):
|
|
194
|
+
name = field.name
|
|
195
|
+
description = hints[name].__metadata__[0]
|
|
196
|
+
chunks.append(name)
|
|
197
|
+
chunks.append(
|
|
198
|
+
textwrap.fill(
|
|
199
|
+
description,
|
|
200
|
+
width=width,
|
|
201
|
+
initial_indent=" ",
|
|
202
|
+
subsequent_indent=" ",
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
if name in extras:
|
|
206
|
+
chunks.append("")
|
|
207
|
+
chunks.append(
|
|
208
|
+
textwrap.fill(
|
|
209
|
+
extras[name],
|
|
210
|
+
width=width,
|
|
211
|
+
initial_indent=" ",
|
|
212
|
+
subsequent_indent=" ",
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
return "\n".join(chunks)
|