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.
@@ -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)