tree-annotated-plot 0.4.0__py3-none-any.whl → 0.5.0__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/_config.py +41 -4
- tree_annotated_plot/_label_style.py +349 -0
- tree_annotated_plot/_plot.py +81 -63
- tree_annotated_plot/cli.py +62 -12
- {tree_annotated_plot-0.4.0.dist-info → tree_annotated_plot-0.5.0.dist-info}/METADATA +1 -1
- tree_annotated_plot-0.5.0.dist-info/RECORD +12 -0
- {tree_annotated_plot-0.4.0.dist-info → tree_annotated_plot-0.5.0.dist-info}/WHEEL +1 -1
- tree_annotated_plot-0.4.0.dist-info/RECORD +0 -11
- {tree_annotated_plot-0.4.0.dist-info → tree_annotated_plot-0.5.0.dist-info}/entry_points.txt +0 -0
- {tree_annotated_plot-0.4.0.dist-info → tree_annotated_plot-0.5.0.dist-info}/licenses/LICENSE +0 -0
tree_annotated_plot/_config.py
CHANGED
|
@@ -161,6 +161,28 @@ class PlotConfig:
|
|
|
161
161
|
"when `connect_leader_to_label` is on.",
|
|
162
162
|
] = "normal"
|
|
163
163
|
|
|
164
|
+
strain_label_styles: Annotated[
|
|
165
|
+
list[dict[str, Any]] | None,
|
|
166
|
+
"Style a subset of the strain labels rendered in the tree panel; "
|
|
167
|
+
"requires `connect_leader_to_label`. A list of rules, each a dict "
|
|
168
|
+
'with keys "strains" (a list of tip identifiers, i.e. '
|
|
169
|
+
'`tree_strain_field` values), "style" and, optionally, "when". '
|
|
170
|
+
'"style" holds Vega-Lite text-mark properties: "fill" (a color), '
|
|
171
|
+
'"opacity" (0 to 1), "fontWeight" ("normal", "bold", or 100-900) '
|
|
172
|
+
'and "fontStyle" ("normal" or "italic"). "when" is a Vega '
|
|
173
|
+
"expression that turns the rule on while it is truthy, usually just "
|
|
174
|
+
"the name of a parameter declared in the chart (e.g. one bound to a "
|
|
175
|
+
"checkbox), but any expression works (e.g. \"emphasis == 'cell'\" "
|
|
176
|
+
'for a radio button). Without "when" the style always applies. '
|
|
177
|
+
"Where rules overlap, later rules win property by property; "
|
|
178
|
+
"properties no rule sets keep `strain_label_font_weight` and the "
|
|
179
|
+
"default black, upright text. A strain that isn't a tree tip is an "
|
|
180
|
+
"error; strains dropped by `prune_tree_to_chart` are skipped. CLI "
|
|
181
|
+
"form: a JSON array (quote the whole argument), e.g. "
|
|
182
|
+
'\'[{"strains": ["K"], "style": {"fill": "#d62728", '
|
|
183
|
+
'"fontWeight": "bold"}, "when": "vaccine_strains"}]\'.',
|
|
184
|
+
] = None
|
|
185
|
+
|
|
164
186
|
shift_tree_loc: Annotated[
|
|
165
187
|
int,
|
|
166
188
|
"Pixels by which to shift the tree toward (positive) or away from "
|
|
@@ -225,10 +247,25 @@ class PlotConfig:
|
|
|
225
247
|
|
|
226
248
|
|
|
227
249
|
# Sidecar for Python-docstring-only prose, keyed by PlotConfig field name.
|
|
228
|
-
#
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
PARAM_DOC_EXTRAS: dict[str, str] = {
|
|
250
|
+
# Add an entry when a field's docstring entry needs more than the CLI
|
|
251
|
+
# `--help` text covers. The extras are appended to the field's description in
|
|
252
|
+
# `tree_annotated_plot.plot.__doc__`, never in CLI --help.
|
|
253
|
+
PARAM_DOC_EXTRAS: dict[str, str] = {
|
|
254
|
+
"strain_label_styles": (
|
|
255
|
+
'In Python, "when" may also be an `altair.Parameter`, e.g. '
|
|
256
|
+
'`alt.param(name="vaccine_strains", value=True, '
|
|
257
|
+
"bind=alt.binding_checkbox())`, which is declared on the returned "
|
|
258
|
+
"chart if your chart doesn't already declare it; or an altair "
|
|
259
|
+
'parameter expression such as `emphasis == "cell"`. Only a bare '
|
|
260
|
+
"`altair.Parameter` is declared for you: parameters that an "
|
|
261
|
+
"expression refers to must be declared on the chart, and an "
|
|
262
|
+
"expression other than a bare name isn't checked until the chart "
|
|
263
|
+
"renders. A bare name or parameter must be a variable parameter: "
|
|
264
|
+
"selection parameters are rejected, since the label data doesn't "
|
|
265
|
+
"carry the fields a selection tests. Any iterable of strings (e.g. "
|
|
266
|
+
'a pandas Series) works for "strains".'
|
|
267
|
+
),
|
|
268
|
+
}
|
|
232
269
|
|
|
233
270
|
|
|
234
271
|
# Descriptions for the three data-input parameters. These can't live on
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""Per-strain styling of the tree-panel strain labels (`strain_label_styles`).
|
|
2
|
+
|
|
3
|
+
Public entry points: :func:`resolve_rules` validates and normalizes the
|
|
4
|
+
user's rules, and :func:`mark_props` turns them into label mark properties.
|
|
5
|
+
|
|
6
|
+
Each rule names some strains, a style (a subset of Vega-Lite text-mark
|
|
7
|
+
properties) and, optionally, a `when` condition. Every property a rule sets
|
|
8
|
+
becomes one `alt.ExprRef` on the existing label mark(s), evaluated per label:
|
|
9
|
+
Vega-Lite compiles a mark-level ``{"expr": ...}`` into the Vega encode block,
|
|
10
|
+
where ``datum`` is the label's row. That is why a property with no encoding
|
|
11
|
+
channel (``fontWeight``, ``fontStyle``) can still vary by strain without
|
|
12
|
+
duplicating the label layers. Which rules a label belongs to is carried in
|
|
13
|
+
one boolean column per rule (:func:`membership_column`) on the label data.
|
|
14
|
+
|
|
15
|
+
``fontSize`` is deliberately not a style key: the label strip's width is
|
|
16
|
+
estimated from `strain_label_font_size`, so a per-strain size would need a
|
|
17
|
+
different layout calculation.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import math
|
|
24
|
+
import re
|
|
25
|
+
from collections.abc import Iterable, Mapping
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
import altair as alt
|
|
30
|
+
from altair.expr.core import Expression
|
|
31
|
+
|
|
32
|
+
# Allowed `style` keys, in the order their expressions are emitted.
|
|
33
|
+
STYLE_KEYS = ("fill", "opacity", "fontWeight", "fontStyle")
|
|
34
|
+
|
|
35
|
+
# Style keys that change the glyph outline. The white halo layer drawn under
|
|
36
|
+
# each label has to follow these so it keeps masking the leader exactly where
|
|
37
|
+
# the text is; `fill` and `opacity` apply to the visible text only.
|
|
38
|
+
_GLYPH_KEYS = frozenset({"fontWeight", "fontStyle"})
|
|
39
|
+
|
|
40
|
+
_RULE_KEYS = frozenset({"strains", "style", "when"})
|
|
41
|
+
|
|
42
|
+
_FONT_WEIGHTS = ("normal", "bold")
|
|
43
|
+
_NUMERIC_FONT_WEIGHTS = tuple(range(100, 1000, 100))
|
|
44
|
+
_FONT_STYLES = ("normal", "italic")
|
|
45
|
+
|
|
46
|
+
# Hints for style keys a user is likely to reach for that we don't accept.
|
|
47
|
+
_STYLE_KEY_HINTS = {
|
|
48
|
+
"color": 'use "fill" to set the label color',
|
|
49
|
+
"fontSize": (
|
|
50
|
+
"per-strain font sizes are not supported because they change the "
|
|
51
|
+
"label-strip layout; use `strain_label_font_size` for all labels"
|
|
52
|
+
),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# A bare Vega expression identifier, i.e. what a param name looks like.
|
|
56
|
+
_IDENTIFIER = re.compile(r"[A-Za-z_$][A-Za-z0-9_$]*")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class LabelStyleRule:
|
|
61
|
+
"""One validated `strain_label_styles` rule.
|
|
62
|
+
|
|
63
|
+
`strains` holds only the strains drawn in the tree (pruned ones are
|
|
64
|
+
dropped); `when` is a Vega expression string, or None for always-on.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
strains: frozenset[str]
|
|
68
|
+
style: dict[str, Any]
|
|
69
|
+
when: str | None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def membership_column(index: int) -> str:
|
|
73
|
+
"""Name of the boolean label-data column marking rule `index`'s strains."""
|
|
74
|
+
return f"_tap_label_style_{index}"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def resolve_rules(
|
|
78
|
+
raw: Any,
|
|
79
|
+
*,
|
|
80
|
+
input_tip_names: Iterable[str],
|
|
81
|
+
drawn_tip_names: Iterable[str],
|
|
82
|
+
chart_spec: dict,
|
|
83
|
+
tree_strain_field: str,
|
|
84
|
+
) -> tuple[list[LabelStyleRule], list[alt.Parameter]]:
|
|
85
|
+
"""Validate `strain_label_styles` and return ``(rules, params_to_declare)``.
|
|
86
|
+
|
|
87
|
+
`input_tip_names` are the tree's tips before `prune_tree_to_chart`; a
|
|
88
|
+
strain outside them is an error. `drawn_tip_names` are the tips left
|
|
89
|
+
after pruning; strains pruned away are silently dropped from their rule.
|
|
90
|
+
`params_to_declare` lists the `alt.Parameter` objects passed as `when`
|
|
91
|
+
that the chart doesn't already declare, deduplicated by name.
|
|
92
|
+
"""
|
|
93
|
+
if raw is None:
|
|
94
|
+
return [], []
|
|
95
|
+
if isinstance(raw, (str, bytes, Mapping)) or not isinstance(raw, Iterable):
|
|
96
|
+
raise ValueError(
|
|
97
|
+
"strain_label_styles must be a list of rule dicts, each with "
|
|
98
|
+
f'"strains" and "style" keys; got {type(raw).__name__}.'
|
|
99
|
+
)
|
|
100
|
+
raw_rules = list(raw)
|
|
101
|
+
if not raw_rules:
|
|
102
|
+
return [], []
|
|
103
|
+
|
|
104
|
+
input_tips = set(input_tip_names)
|
|
105
|
+
drawn_tips = set(drawn_tip_names)
|
|
106
|
+
declared = _declared_params(chart_spec)
|
|
107
|
+
rule_params: dict[str, alt.Parameter] = {}
|
|
108
|
+
for rule in raw_rules:
|
|
109
|
+
when = rule.get("when") if isinstance(rule, Mapping) else None
|
|
110
|
+
if isinstance(when, alt.Parameter):
|
|
111
|
+
rule_params.setdefault(_param_name(when), when)
|
|
112
|
+
|
|
113
|
+
rules: list[LabelStyleRule] = []
|
|
114
|
+
for i, rule in enumerate(raw_rules):
|
|
115
|
+
if not isinstance(rule, Mapping):
|
|
116
|
+
raise ValueError(
|
|
117
|
+
f"strain_label_styles[{i}] must be a dict with "
|
|
118
|
+
f'"strains" and "style" keys; got {type(rule).__name__}.'
|
|
119
|
+
)
|
|
120
|
+
unknown_keys = set(rule) - _RULE_KEYS
|
|
121
|
+
if unknown_keys:
|
|
122
|
+
raise ValueError(
|
|
123
|
+
f"strain_label_styles[{i}] has unknown key(s) "
|
|
124
|
+
f"{sorted(unknown_keys)}; allowed keys are "
|
|
125
|
+
f"{sorted(_RULE_KEYS)}."
|
|
126
|
+
)
|
|
127
|
+
for required in ("strains", "style"):
|
|
128
|
+
if required not in rule:
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"strain_label_styles[{i}] is missing the required "
|
|
131
|
+
f"{required!r} key."
|
|
132
|
+
)
|
|
133
|
+
rules.append(
|
|
134
|
+
LabelStyleRule(
|
|
135
|
+
strains=_resolve_strains(
|
|
136
|
+
i,
|
|
137
|
+
rule["strains"],
|
|
138
|
+
input_tips=input_tips,
|
|
139
|
+
drawn_tips=drawn_tips,
|
|
140
|
+
tree_strain_field=tree_strain_field,
|
|
141
|
+
),
|
|
142
|
+
style=_validate_style(i, rule["style"]),
|
|
143
|
+
when=_resolve_when(
|
|
144
|
+
i,
|
|
145
|
+
rule.get("when"),
|
|
146
|
+
declared=declared,
|
|
147
|
+
rule_param_names=set(rule_params),
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
to_declare = [p for name, p in rule_params.items() if name not in declared]
|
|
153
|
+
return rules, to_declare
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def mark_props(
|
|
157
|
+
rules: list[LabelStyleRule], *, default_font_weight: str
|
|
158
|
+
) -> tuple[dict[str, alt.ExprRef], dict[str, alt.ExprRef]]:
|
|
159
|
+
"""Return ``(halo_props, text_props)``: mark properties for the label layers.
|
|
160
|
+
|
|
161
|
+
Only properties that some rule sets get an expression; the rest keep the
|
|
162
|
+
marks' static settings. Each expression is a chain of ternaries built in
|
|
163
|
+
rule order, so the last rule's test is evaluated first and later rules
|
|
164
|
+
win where strains overlap. The fallback is the label's unstyled value:
|
|
165
|
+
`default_font_weight` for ``fontWeight``, and Vega-Lite's text defaults
|
|
166
|
+
(black, fully opaque, upright) for the others.
|
|
167
|
+
"""
|
|
168
|
+
defaults = {
|
|
169
|
+
"fill": "black",
|
|
170
|
+
"opacity": 1,
|
|
171
|
+
"fontWeight": default_font_weight,
|
|
172
|
+
"fontStyle": "normal",
|
|
173
|
+
}
|
|
174
|
+
text_props: dict[str, alt.ExprRef] = {}
|
|
175
|
+
for key in STYLE_KEYS:
|
|
176
|
+
expr = json.dumps(defaults[key])
|
|
177
|
+
styled = False
|
|
178
|
+
for i, rule in enumerate(rules):
|
|
179
|
+
if key not in rule.style:
|
|
180
|
+
continue
|
|
181
|
+
test = f"datum[{json.dumps(membership_column(i))}]"
|
|
182
|
+
if rule.when is not None:
|
|
183
|
+
test = f"({rule.when}) && {test}"
|
|
184
|
+
expr = f"{test} ? {json.dumps(rule.style[key])} : ({expr})"
|
|
185
|
+
styled = True
|
|
186
|
+
if styled:
|
|
187
|
+
text_props[key] = alt.ExprRef(expr)
|
|
188
|
+
halo_props = {k: v for k, v in text_props.items() if k in _GLYPH_KEYS}
|
|
189
|
+
return halo_props, text_props
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _resolve_strains(
|
|
193
|
+
index: int,
|
|
194
|
+
strains: Any,
|
|
195
|
+
*,
|
|
196
|
+
input_tips: set[str],
|
|
197
|
+
drawn_tips: set[str],
|
|
198
|
+
tree_strain_field: str,
|
|
199
|
+
) -> frozenset[str]:
|
|
200
|
+
if isinstance(strains, (str, bytes, Mapping)) or not isinstance(strains, Iterable):
|
|
201
|
+
raise ValueError(
|
|
202
|
+
f'strain_label_styles[{index}]["strains"] must be a list of '
|
|
203
|
+
f"strain names; got {type(strains).__name__} {strains!r}."
|
|
204
|
+
)
|
|
205
|
+
strains = list(strains)
|
|
206
|
+
if not strains:
|
|
207
|
+
raise ValueError(
|
|
208
|
+
f'strain_label_styles[{index}]["strains"] is empty; a rule must '
|
|
209
|
+
"name at least one strain."
|
|
210
|
+
)
|
|
211
|
+
non_str = [s for s in strains if not isinstance(s, str)]
|
|
212
|
+
if non_str:
|
|
213
|
+
raise ValueError(
|
|
214
|
+
f'strain_label_styles[{index}]["strains"] must contain strings; '
|
|
215
|
+
f"got {non_str[:5]!r}."
|
|
216
|
+
)
|
|
217
|
+
unknown = sorted(set(strains) - input_tips)
|
|
218
|
+
if unknown:
|
|
219
|
+
raise ValueError(
|
|
220
|
+
f'strain_label_styles[{index}]["strains"] has {len(unknown)} '
|
|
221
|
+
"strain(s) that are not tips of the tree under "
|
|
222
|
+
f"tree_strain_field={tree_strain_field!r}.\n"
|
|
223
|
+
f"Sample unknown strains: {unknown[:5]}\n"
|
|
224
|
+
f"Sample tree tips: {sorted(input_tips)[:5]}"
|
|
225
|
+
)
|
|
226
|
+
return frozenset(s for s in strains if s in drawn_tips)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _validate_style(index: int, style: Any) -> dict[str, Any]:
|
|
230
|
+
where = f'strain_label_styles[{index}]["style"]'
|
|
231
|
+
if not isinstance(style, Mapping) or not style:
|
|
232
|
+
raise ValueError(
|
|
233
|
+
f"{where} must be a non-empty dict of label properties (any of "
|
|
234
|
+
f"{list(STYLE_KEYS)}); got {style!r}."
|
|
235
|
+
)
|
|
236
|
+
for key, value in style.items():
|
|
237
|
+
if key not in STYLE_KEYS:
|
|
238
|
+
hint = _STYLE_KEY_HINTS.get(key)
|
|
239
|
+
raise ValueError(
|
|
240
|
+
f"{where} has unsupported key {key!r}; allowed keys are "
|
|
241
|
+
f"{list(STYLE_KEYS)}." + (f" Hint: {hint}." if hint else "")
|
|
242
|
+
)
|
|
243
|
+
if key == "fill":
|
|
244
|
+
ok = isinstance(value, str) and bool(value.strip())
|
|
245
|
+
expected = "a non-empty color string"
|
|
246
|
+
elif key == "opacity":
|
|
247
|
+
ok = (
|
|
248
|
+
isinstance(value, (int, float))
|
|
249
|
+
and not isinstance(value, bool)
|
|
250
|
+
and math.isfinite(value)
|
|
251
|
+
and 0 <= value <= 1
|
|
252
|
+
)
|
|
253
|
+
expected = "a number from 0 to 1"
|
|
254
|
+
elif key == "fontWeight":
|
|
255
|
+
ok = value in _FONT_WEIGHTS or (
|
|
256
|
+
isinstance(value, int)
|
|
257
|
+
and not isinstance(value, bool)
|
|
258
|
+
and value in _NUMERIC_FONT_WEIGHTS
|
|
259
|
+
)
|
|
260
|
+
expected = f"one of {list(_FONT_WEIGHTS)} or 100-900 in steps of 100"
|
|
261
|
+
else: # fontStyle
|
|
262
|
+
ok = value in _FONT_STYLES
|
|
263
|
+
expected = f"one of {list(_FONT_STYLES)}"
|
|
264
|
+
if not ok:
|
|
265
|
+
raise ValueError(f"{where}[{key!r}] must be {expected}; got {value!r}.")
|
|
266
|
+
return dict(style)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _resolve_when(
|
|
270
|
+
index: int,
|
|
271
|
+
when: Any,
|
|
272
|
+
*,
|
|
273
|
+
declared: dict[str, dict],
|
|
274
|
+
rule_param_names: set[str],
|
|
275
|
+
) -> str | None:
|
|
276
|
+
"""Return `when` as a Vega expression string (None means always on)."""
|
|
277
|
+
where = f'strain_label_styles[{index}]["when"]'
|
|
278
|
+
if when is None:
|
|
279
|
+
return None
|
|
280
|
+
if isinstance(when, alt.Parameter):
|
|
281
|
+
if when.param_type != "variable":
|
|
282
|
+
raise ValueError(
|
|
283
|
+
f"{where} is a selection parameter ({_param_name(when)!r}); "
|
|
284
|
+
"only variable parameters (alt.param(...), e.g. bound to a "
|
|
285
|
+
"checkbox) are supported, since the label data doesn't carry "
|
|
286
|
+
"the fields a selection tests."
|
|
287
|
+
)
|
|
288
|
+
return _param_name(when)
|
|
289
|
+
if isinstance(when, alt.ParameterExpression):
|
|
290
|
+
expr = when.to_dict().get("expr")
|
|
291
|
+
elif isinstance(when, Expression):
|
|
292
|
+
expr = when.to_dict()
|
|
293
|
+
elif isinstance(when, str):
|
|
294
|
+
expr = when
|
|
295
|
+
else:
|
|
296
|
+
raise ValueError(
|
|
297
|
+
f"{where} must be a param name or Vega expression string, an "
|
|
298
|
+
"alt.Parameter, or an altair parameter expression; got "
|
|
299
|
+
f"{type(when).__name__}."
|
|
300
|
+
)
|
|
301
|
+
if not isinstance(expr, str) or not expr.strip():
|
|
302
|
+
raise ValueError(f"{where} must not be empty; got {when!r}.")
|
|
303
|
+
expr = expr.strip()
|
|
304
|
+
if _IDENTIFIER.fullmatch(expr) and expr not in rule_param_names:
|
|
305
|
+
if expr not in declared:
|
|
306
|
+
raise ValueError(
|
|
307
|
+
f"{where}={expr!r} names no parameter declared in the chart. "
|
|
308
|
+
f"Declared parameters: {sorted(declared) or 'none'}. Declare "
|
|
309
|
+
"it on the chart (e.g. chart.add_params(alt.param(name="
|
|
310
|
+
f"{expr!r}, value=True, bind=alt.binding_checkbox()))) before "
|
|
311
|
+
"saving it, or, in Python, pass the alt.Parameter itself."
|
|
312
|
+
)
|
|
313
|
+
if "select" in declared[expr]:
|
|
314
|
+
raise ValueError(
|
|
315
|
+
f"{where}={expr!r} is a selection parameter; only variable "
|
|
316
|
+
"parameters (e.g. bound to a checkbox) are supported, since "
|
|
317
|
+
"the label data doesn't carry the fields a selection tests."
|
|
318
|
+
)
|
|
319
|
+
return expr
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _declared_params(spec: Any) -> dict[str, dict]:
|
|
323
|
+
"""Map every param name declared anywhere in a chart spec to its definition."""
|
|
324
|
+
found: dict[str, dict] = {}
|
|
325
|
+
|
|
326
|
+
def walk(node: Any) -> None:
|
|
327
|
+
if isinstance(node, dict):
|
|
328
|
+
params = node.get("params")
|
|
329
|
+
if isinstance(params, list):
|
|
330
|
+
for p in params:
|
|
331
|
+
if isinstance(p, dict) and isinstance(p.get("name"), str):
|
|
332
|
+
found.setdefault(p["name"], p)
|
|
333
|
+
for k, v in node.items():
|
|
334
|
+
if k in ("data", "datasets"):
|
|
335
|
+
continue
|
|
336
|
+
walk(v)
|
|
337
|
+
elif isinstance(node, list):
|
|
338
|
+
for item in node:
|
|
339
|
+
walk(item)
|
|
340
|
+
|
|
341
|
+
walk(spec)
|
|
342
|
+
return found
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _param_name(param: alt.Parameter) -> str:
|
|
346
|
+
name = param.name
|
|
347
|
+
if hasattr(name, "to_dict"):
|
|
348
|
+
name = name.to_dict()
|
|
349
|
+
return str(name)
|
tree_annotated_plot/_plot.py
CHANGED
|
@@ -15,7 +15,7 @@ from typing import Any, Literal
|
|
|
15
15
|
import altair as alt
|
|
16
16
|
import pandas as pd
|
|
17
17
|
|
|
18
|
-
from . import _color, _config, _tree
|
|
18
|
+
from . import _color, _config, _label_style, _tree
|
|
19
19
|
from ._config import PlotConfig, TreeLocation
|
|
20
20
|
|
|
21
21
|
# Vega-Lite `orient` values that anchor the legend to the chart's left or
|
|
@@ -61,6 +61,7 @@ def plot(
|
|
|
61
61
|
connect_leader_to_label: bool = False,
|
|
62
62
|
strain_label_font_size: float = 10.0,
|
|
63
63
|
strain_label_font_weight: Literal["normal", "bold"] = "normal",
|
|
64
|
+
strain_label_styles: list[dict[str, Any]] | None = None,
|
|
64
65
|
shift_tree_loc: int = 0,
|
|
65
66
|
color_tree_by: str | None = None,
|
|
66
67
|
tree_color_scale: dict[str, str] | None = None,
|
|
@@ -90,6 +91,7 @@ def plot(
|
|
|
90
91
|
connect_leader_to_label=connect_leader_to_label,
|
|
91
92
|
strain_label_font_size=strain_label_font_size,
|
|
92
93
|
strain_label_font_weight=strain_label_font_weight,
|
|
94
|
+
strain_label_styles=strain_label_styles,
|
|
93
95
|
shift_tree_loc=shift_tree_loc,
|
|
94
96
|
color_tree_by=color_tree_by,
|
|
95
97
|
tree_color_scale=tree_color_scale,
|
|
@@ -124,10 +126,12 @@ _PLOT_DOC_FOOTER = """
|
|
|
124
126
|
Returns
|
|
125
127
|
-------
|
|
126
128
|
altair.HConcatChart | altair.VConcatChart
|
|
127
|
-
For vertical layout (chart strain on `y`), an `HConcatChart`
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
For vertical layout (chart strain on `y`), an `HConcatChart` whose
|
|
130
|
+
panels are the tree then the chart for `tree_location="left"` (the
|
|
131
|
+
default), or the chart then the tree for `"right"`. For horizontal
|
|
132
|
+
layout (chart strain on `x`), a `VConcatChart` whose panels are the
|
|
133
|
+
chart then the tree for `tree_location="bottom"` (the default), or the
|
|
134
|
+
tree then the chart for `"top"`.
|
|
131
135
|
"""
|
|
132
136
|
|
|
133
137
|
|
|
@@ -149,6 +153,14 @@ def _build(
|
|
|
149
153
|
set on a config object); `config` carries every styling / behavior
|
|
150
154
|
knob. Both surfaces converge here so they can never disagree.
|
|
151
155
|
"""
|
|
156
|
+
if config.strain_label_styles and not config.connect_leader_to_label:
|
|
157
|
+
raise ValueError(
|
|
158
|
+
"strain_label_styles styles the strain labels rendered in the "
|
|
159
|
+
"tree panel, which exist only when connect_leader_to_label=True. "
|
|
160
|
+
"With it off, the strain labels are your chart's own axis labels; "
|
|
161
|
+
"style those with Vega-Lite's conditional axis-label properties "
|
|
162
|
+
"(e.g. labelColor / labelFontWeight expressions on datum.value)."
|
|
163
|
+
)
|
|
152
164
|
root, auspice_meta = _ensure_tree(
|
|
153
165
|
tree,
|
|
154
166
|
config.tree_strain_field,
|
|
@@ -157,6 +169,9 @@ def _build(
|
|
|
157
169
|
)
|
|
158
170
|
tip_list = _tree.layout(root)
|
|
159
171
|
tip_names = [t.name for t in tip_list]
|
|
172
|
+
# Kept for `strain_label_styles` validation: a styled strain must be a tip
|
|
173
|
+
# of the input tree even if `prune_tree_to_chart` later drops it.
|
|
174
|
+
input_tip_names = list(tip_names)
|
|
160
175
|
|
|
161
176
|
_check_no_duplicate_tip_strains(
|
|
162
177
|
tip_names, tree_strain_field=config.tree_strain_field
|
|
@@ -208,6 +223,14 @@ def _build(
|
|
|
208
223
|
tip_list = _tree.layout(root)
|
|
209
224
|
tip_names = [t.name for t in tip_list]
|
|
210
225
|
|
|
226
|
+
label_style_rules, label_style_params = _label_style.resolve_rules(
|
|
227
|
+
config.strain_label_styles,
|
|
228
|
+
input_tip_names=input_tip_names,
|
|
229
|
+
drawn_tip_names=tip_names,
|
|
230
|
+
chart_spec=spec,
|
|
231
|
+
tree_strain_field=config.tree_strain_field,
|
|
232
|
+
)
|
|
233
|
+
|
|
211
234
|
strain_dim = _coerce_dim(
|
|
212
235
|
_chart_strain_dim(spec, axis_hits, axis), n_tips=len(tip_names)
|
|
213
236
|
)
|
|
@@ -242,6 +265,7 @@ def _build(
|
|
|
242
265
|
connect_leader_to_label=config.connect_leader_to_label,
|
|
243
266
|
strain_label_font_size=config.strain_label_font_size,
|
|
244
267
|
strain_label_font_weight=config.strain_label_font_weight,
|
|
268
|
+
label_style_rules=label_style_rules,
|
|
245
269
|
shift_tree_loc=config.shift_tree_loc,
|
|
246
270
|
tip_names=tip_names,
|
|
247
271
|
color_mapping=color_mapping,
|
|
@@ -291,6 +315,15 @@ def _build(
|
|
|
291
315
|
_apply_combined_config(combined, hoisted_config)
|
|
292
316
|
for k, v in hoisted_other.items():
|
|
293
317
|
combined._kwds[k] = v
|
|
318
|
+
# Declare any `when` parameter the user's chart doesn't already declare.
|
|
319
|
+
# Appended to the top-level `params` directly: `combined.add_params(...)`
|
|
320
|
+
# pushes params down into each sub-chart instead, which raises on charts
|
|
321
|
+
# loaded from a spec (e.g. a `FacetSpec` has no `add_params`).
|
|
322
|
+
if label_style_params:
|
|
323
|
+
existing = _live_attr(combined, "params") or []
|
|
324
|
+
combined._kwds["params"] = list(existing) + [
|
|
325
|
+
p.param for p in label_style_params
|
|
326
|
+
]
|
|
294
327
|
return combined
|
|
295
328
|
|
|
296
329
|
|
|
@@ -390,9 +423,15 @@ def _load_chart(chart_input: ChartInput, *, strict_version: bool) -> alt.TopLeve
|
|
|
390
423
|
A live `alt.TopLevelMixin` is returned as-is — its `$schema` is
|
|
391
424
|
irrelevant since the constructing altair version is necessarily the
|
|
392
425
|
running altair version. For dict / JSON path / HTML path inputs we run
|
|
393
|
-
the Vega-Lite version check (under `strict_version`)
|
|
394
|
-
|
|
395
|
-
|
|
426
|
+
the Vega-Lite version check (under `strict_version`) and then dispatch to
|
|
427
|
+
the right chart subclass via `alt.Chart.from_dict(...)`.
|
|
428
|
+
|
|
429
|
+
The spec goes to `from_dict` unmodified. In particular, each selection
|
|
430
|
+
param's `views` must survive: when `_build` nests a compound chart (layer
|
|
431
|
+
/ facet / concat), altair keeps its params' existing `views`, and an
|
|
432
|
+
emptied list makes Vega-Lite define the selection in every view, which
|
|
433
|
+
fails with "Duplicate signal name". `views` is valid on top-level params,
|
|
434
|
+
which is the only place altair emits it.
|
|
396
435
|
"""
|
|
397
436
|
if isinstance(chart_input, alt.TopLevelMixin):
|
|
398
437
|
return chart_input
|
|
@@ -420,8 +459,9 @@ def _load_chart(chart_input: ChartInput, *, strict_version: bool) -> alt.TopLeve
|
|
|
420
459
|
)
|
|
421
460
|
|
|
422
461
|
_check_chart_schema_version(spec, strict_version=strict_version)
|
|
462
|
+
# `from_dict` keeps references into the spec (e.g. the `datasets` row
|
|
463
|
+
# lists), so copy it to keep the caller's dict out of later mutation.
|
|
423
464
|
spec = copy.deepcopy(spec)
|
|
424
|
-
_strip_params_views(spec)
|
|
425
465
|
# `from_dict` with validate=True dispatches to the right subclass
|
|
426
466
|
# (Chart / LayerChart / FacetChart / HConcatChart / VConcatChart /
|
|
427
467
|
# ConcatChart) based on the spec's shape.
|
|
@@ -485,26 +525,6 @@ def _check_chart_schema_version(spec: dict, *, strict_version: bool) -> None:
|
|
|
485
525
|
)
|
|
486
526
|
|
|
487
527
|
|
|
488
|
-
def _strip_params_views(spec: Any) -> None:
|
|
489
|
-
"""Recursively delete `views` from every params[] entry.
|
|
490
|
-
|
|
491
|
-
altair's `to_dict()` annotates each selection-param with the views it's
|
|
492
|
-
bound to, but altair's own `from_dict()` validation rejects the field.
|
|
493
|
-
Stripping is safe: the renderer rebinds params to views on its own.
|
|
494
|
-
"""
|
|
495
|
-
if isinstance(spec, dict):
|
|
496
|
-
params = spec.get("params")
|
|
497
|
-
if isinstance(params, list):
|
|
498
|
-
for p in params:
|
|
499
|
-
if isinstance(p, dict):
|
|
500
|
-
p.pop("views", None)
|
|
501
|
-
for v in spec.values():
|
|
502
|
-
_strip_params_views(v)
|
|
503
|
-
elif isinstance(spec, list):
|
|
504
|
-
for item in spec:
|
|
505
|
-
_strip_params_views(item)
|
|
506
|
-
|
|
507
|
-
|
|
508
528
|
class _ScriptCollector(HTMLParser):
|
|
509
529
|
"""Collect the text content of every <script> tag in an HTML document."""
|
|
510
530
|
|
|
@@ -1508,6 +1528,7 @@ def _build_tree_chart(
|
|
|
1508
1528
|
connect_leader_to_label: bool = False,
|
|
1509
1529
|
strain_label_font_size: float = 10.0,
|
|
1510
1530
|
strain_label_font_weight: str = "normal",
|
|
1531
|
+
label_style_rules: list[_label_style.LabelStyleRule] | None = None,
|
|
1511
1532
|
shift_tree_loc: int = 0,
|
|
1512
1533
|
tip_names: list[str] | None = None,
|
|
1513
1534
|
color_mapping: _color.ColorMapping | None = None,
|
|
@@ -1613,6 +1634,10 @@ def _build_tree_chart(
|
|
|
1613
1634
|
# text — and masks the dashed leader behind each label without any
|
|
1614
1635
|
# width estimation. Vega-Lite doesn't expose `paintOrder`, so the
|
|
1615
1636
|
# two-layer trick stands in for a single text-with-halo mark.
|
|
1637
|
+
# - `strain_label_styles` rules restyle individual labels by replacing
|
|
1638
|
+
# these layers' mark properties with per-label `datum` expressions
|
|
1639
|
+
# (see `_label_style`); the halo follows the glyph-shape ones
|
|
1640
|
+
# (fontWeight / fontStyle) so it keeps matching the text.
|
|
1616
1641
|
# - `shift_tree_loc` (pixels) shrinks the strip — bringing the tree
|
|
1617
1642
|
# visually closer to the labels — by reducing the data-units between
|
|
1618
1643
|
# branch_max and chart_edge_branch. The tree's tree_size-pixel
|
|
@@ -1620,6 +1645,23 @@ def _build_tree_chart(
|
|
|
1620
1645
|
# When connect_leader_to_label is off, the chart's natural strain-axis
|
|
1621
1646
|
# labels are kept and leaders stop at branch_max (the prior behavior).
|
|
1622
1647
|
halo_px = max(2.0, strain_label_font_size * _LABEL_HALO_RATIO)
|
|
1648
|
+
halo_style, text_style = _label_style.mark_props(
|
|
1649
|
+
label_style_rules or [], default_font_weight=strain_label_font_weight
|
|
1650
|
+
)
|
|
1651
|
+
label_font_kwargs = {
|
|
1652
|
+
"baseline": "middle",
|
|
1653
|
+
"fontSize": strain_label_font_size,
|
|
1654
|
+
"fontWeight": strain_label_font_weight,
|
|
1655
|
+
}
|
|
1656
|
+
label_halo_mark_kwargs = {
|
|
1657
|
+
**label_font_kwargs,
|
|
1658
|
+
"fill": "white",
|
|
1659
|
+
"stroke": "white",
|
|
1660
|
+
"strokeWidth": halo_px,
|
|
1661
|
+
"strokeJoin": "round",
|
|
1662
|
+
**halo_style,
|
|
1663
|
+
}
|
|
1664
|
+
label_text_mark_kwargs = {**label_font_kwargs, **text_style}
|
|
1623
1665
|
if connect_leader_to_label:
|
|
1624
1666
|
names = tip_names if tip_names is not None else list(tips_df["name"])
|
|
1625
1667
|
max_name_len = max((len(n) for n in names), default=0)
|
|
@@ -1654,6 +1696,12 @@ def _build_tree_chart(
|
|
|
1654
1696
|
leader_df = tips_df[tips_df["x"] < label_anchor_branch].assign(
|
|
1655
1697
|
x2=label_anchor_branch
|
|
1656
1698
|
)
|
|
1699
|
+
tips_df = tips_df.assign(
|
|
1700
|
+
**{
|
|
1701
|
+
_label_style.membership_column(i): tips_df["name"].isin(rule.strains)
|
|
1702
|
+
for i, rule in enumerate(label_style_rules or [])
|
|
1703
|
+
}
|
|
1704
|
+
)
|
|
1657
1705
|
extended_tree_size = tree_size + strip_pixel_width
|
|
1658
1706
|
else:
|
|
1659
1707
|
chart_edge_branch = branch_max
|
|
@@ -1769,26 +1817,12 @@ def _build_tree_chart(
|
|
|
1769
1817
|
)
|
|
1770
1818
|
layers.append(
|
|
1771
1819
|
alt.Chart(tips_df)
|
|
1772
|
-
.mark_text(
|
|
1773
|
-
align=text_align,
|
|
1774
|
-
baseline="middle",
|
|
1775
|
-
fontSize=strain_label_font_size,
|
|
1776
|
-
fontWeight=strain_label_font_weight,
|
|
1777
|
-
fill="white",
|
|
1778
|
-
stroke="white",
|
|
1779
|
-
strokeWidth=halo_px,
|
|
1780
|
-
strokeJoin="round",
|
|
1781
|
-
)
|
|
1820
|
+
.mark_text(align=text_align, **label_halo_mark_kwargs)
|
|
1782
1821
|
.encode(**label_text_enc)
|
|
1783
1822
|
)
|
|
1784
1823
|
layers.append(
|
|
1785
1824
|
alt.Chart(tips_df)
|
|
1786
|
-
.mark_text(
|
|
1787
|
-
align=text_align,
|
|
1788
|
-
baseline="middle",
|
|
1789
|
-
fontSize=strain_label_font_size,
|
|
1790
|
-
fontWeight=strain_label_font_weight,
|
|
1791
|
-
)
|
|
1825
|
+
.mark_text(align=text_align, **label_text_mark_kwargs)
|
|
1792
1826
|
.encode(**label_text_enc)
|
|
1793
1827
|
)
|
|
1794
1828
|
if scale_bar_layer is not None:
|
|
@@ -1875,28 +1909,12 @@ def _build_tree_chart(
|
|
|
1875
1909
|
)
|
|
1876
1910
|
layers.append(
|
|
1877
1911
|
alt.Chart(tips_df)
|
|
1878
|
-
.mark_text(
|
|
1879
|
-
align=text_align,
|
|
1880
|
-
baseline="middle",
|
|
1881
|
-
angle=270,
|
|
1882
|
-
fontSize=strain_label_font_size,
|
|
1883
|
-
fontWeight=strain_label_font_weight,
|
|
1884
|
-
fill="white",
|
|
1885
|
-
stroke="white",
|
|
1886
|
-
strokeWidth=halo_px,
|
|
1887
|
-
strokeJoin="round",
|
|
1888
|
-
)
|
|
1912
|
+
.mark_text(align=text_align, angle=270, **label_halo_mark_kwargs)
|
|
1889
1913
|
.encode(**label_text_enc)
|
|
1890
1914
|
)
|
|
1891
1915
|
layers.append(
|
|
1892
1916
|
alt.Chart(tips_df)
|
|
1893
|
-
.mark_text(
|
|
1894
|
-
align=text_align,
|
|
1895
|
-
baseline="middle",
|
|
1896
|
-
angle=270,
|
|
1897
|
-
fontSize=strain_label_font_size,
|
|
1898
|
-
fontWeight=strain_label_font_weight,
|
|
1899
|
-
)
|
|
1917
|
+
.mark_text(align=text_align, angle=270, **label_text_mark_kwargs)
|
|
1900
1918
|
.encode(**label_text_enc)
|
|
1901
1919
|
)
|
|
1902
1920
|
if scale_bar_layer is not None:
|
tree_annotated_plot/cli.py
CHANGED
|
@@ -74,19 +74,26 @@ class _ColorScaleParamType(click.ParamType):
|
|
|
74
74
|
_COLOR_SCALE_PARAM_TYPE = _ColorScaleParamType()
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
class
|
|
78
|
-
"""Parse a JSON
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
``'{"orient":"left","labelFontSize":13}'``).
|
|
83
|
-
|
|
77
|
+
class _JsonParamType(click.ParamType):
|
|
78
|
+
"""Parse a JSON string into a dict, or into a list of dicts.
|
|
79
|
+
|
|
80
|
+
`container=dict` expects a JSON object; used for
|
|
81
|
+
``--tree-color-legend-format``: any subset of Vega-Lite's Legend
|
|
82
|
+
properties (e.g. ``'{"orient":"left","labelFontSize":13}'``).
|
|
83
|
+
`container=list` expects a JSON array of objects; used for
|
|
84
|
+
``--strain-label-styles`` (e.g.
|
|
85
|
+
``'[{"strains":["K"],"style":{"fill":"red"}}]'``). The user must quote
|
|
86
|
+
the whole argument so the shell doesn't interpret braces or quotes.
|
|
87
|
+
`example` is shown in the error message when the JSON has the wrong shape.
|
|
84
88
|
"""
|
|
85
89
|
|
|
86
|
-
name
|
|
90
|
+
def __init__(self, container: type, *, name: str, example: str) -> None:
|
|
91
|
+
self.container = container
|
|
92
|
+
self.name = name
|
|
93
|
+
self.example = example
|
|
87
94
|
|
|
88
95
|
def convert(self, value, param, ctx): # type: ignore[override]
|
|
89
|
-
if value is None or isinstance(value,
|
|
96
|
+
if value is None or isinstance(value, self.container):
|
|
90
97
|
return value
|
|
91
98
|
text = str(value).strip()
|
|
92
99
|
if not text:
|
|
@@ -95,17 +102,34 @@ class _JsonDictParamType(click.ParamType):
|
|
|
95
102
|
parsed = json.loads(text)
|
|
96
103
|
except json.JSONDecodeError as exc:
|
|
97
104
|
self.fail(f"invalid JSON: {exc}", param, ctx)
|
|
98
|
-
if
|
|
105
|
+
kind = "object" if self.container is dict else "array of objects"
|
|
106
|
+
if not isinstance(parsed, self.container):
|
|
99
107
|
self.fail(
|
|
100
|
-
f
|
|
108
|
+
f"expected a JSON {kind} (e.g. {self.example}); "
|
|
101
109
|
f"got {type(parsed).__name__}.",
|
|
102
110
|
param,
|
|
103
111
|
ctx,
|
|
104
112
|
)
|
|
113
|
+
if self.container is list:
|
|
114
|
+
for i, item in enumerate(parsed):
|
|
115
|
+
if not isinstance(item, dict):
|
|
116
|
+
self.fail(
|
|
117
|
+
f"expected a JSON {kind} (e.g. {self.example}); "
|
|
118
|
+
f"element {i} is {type(item).__name__}.",
|
|
119
|
+
param,
|
|
120
|
+
ctx,
|
|
121
|
+
)
|
|
105
122
|
return parsed
|
|
106
123
|
|
|
107
124
|
|
|
108
|
-
_JSON_DICT_PARAM_TYPE =
|
|
125
|
+
_JSON_DICT_PARAM_TYPE = _JsonParamType(
|
|
126
|
+
dict, name="json_dict", example='\'{"orient":"left"}\''
|
|
127
|
+
)
|
|
128
|
+
_JSON_LIST_PARAM_TYPE = _JsonParamType(
|
|
129
|
+
list,
|
|
130
|
+
name="json_list",
|
|
131
|
+
example='\'[{"strains":["K"],"style":{"fill":"red"}}]\'',
|
|
132
|
+
)
|
|
109
133
|
|
|
110
134
|
|
|
111
135
|
def _is_str_dict(tp: Any) -> bool:
|
|
@@ -126,6 +150,14 @@ def _is_dict_any(tp: Any) -> bool:
|
|
|
126
150
|
return False
|
|
127
151
|
|
|
128
152
|
|
|
153
|
+
def _is_list_of_dicts(tp: Any) -> bool:
|
|
154
|
+
"""Return True for `list[dict[str, Any]]`."""
|
|
155
|
+
if get_origin(tp) is list:
|
|
156
|
+
args = get_args(tp)
|
|
157
|
+
return len(args) == 1 and _is_dict_any(args[0])
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
|
|
129
161
|
def _option_for_field(field: dataclasses.Field, hints: dict) -> Any:
|
|
130
162
|
"""Build a `click.option(...)` decorator from a PlotConfig field.
|
|
131
163
|
|
|
@@ -188,6 +220,16 @@ def _option_for_field(field: dataclasses.Field, hints: dict) -> Any:
|
|
|
188
220
|
show_default=False,
|
|
189
221
|
)
|
|
190
222
|
|
|
223
|
+
# list[dict[str, Any]] (with or without `| None`) → JSON-array parser.
|
|
224
|
+
if _is_list_of_dicts(real_type):
|
|
225
|
+
return click.option(
|
|
226
|
+
cli_name,
|
|
227
|
+
type=_JSON_LIST_PARAM_TYPE,
|
|
228
|
+
default=field.default if has_default else None,
|
|
229
|
+
help=description,
|
|
230
|
+
show_default=False,
|
|
231
|
+
)
|
|
232
|
+
|
|
191
233
|
# Optional / Union — unwrap to the non-None branch.
|
|
192
234
|
origin = get_origin(real_type)
|
|
193
235
|
if origin in (typing.Union, types.UnionType):
|
|
@@ -210,6 +252,14 @@ def _option_for_field(field: dataclasses.Field, hints: dict) -> Any:
|
|
|
210
252
|
help=description,
|
|
211
253
|
show_default=False,
|
|
212
254
|
)
|
|
255
|
+
if _is_list_of_dicts(inner):
|
|
256
|
+
return click.option(
|
|
257
|
+
cli_name,
|
|
258
|
+
type=_JSON_LIST_PARAM_TYPE,
|
|
259
|
+
default=field.default if has_default else None,
|
|
260
|
+
help=description,
|
|
261
|
+
show_default=False,
|
|
262
|
+
)
|
|
213
263
|
if get_origin(inner) is Literal:
|
|
214
264
|
choices = [str(c) for c in get_args(inner)]
|
|
215
265
|
return click.option(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tree-annotated-plot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.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
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
tree_annotated_plot/__init__.py,sha256=R39gjVnl3K2VjLba6zwtwPGADFmtNOE2UYNk1y9_z6k,351
|
|
2
|
+
tree_annotated_plot/_color.py,sha256=nOZp-KIPy6s-6gBkzW36oHrwDk_SmvVNsMiIJ-dY1Io,26519
|
|
3
|
+
tree_annotated_plot/_config.py,sha256=sDFaOoTcQ87IG7Yx4bOnWsv44uPHHhk_wvA4xeQ-NoA,15412
|
|
4
|
+
tree_annotated_plot/_label_style.py,sha256=dyayJZeXT7TvldsRvEP-LhZgqJJoez_luqkk8lRAPvE,13281
|
|
5
|
+
tree_annotated_plot/_plot.py,sha256=k8CygwCWa9Fl-EANDzAO-LfE3Wjj2pC0-PwHDIjt7JI,79668
|
|
6
|
+
tree_annotated_plot/_tree.py,sha256=Bj-ABt8aooSwV7wAVTbwFGMmCnTPGOKweUWAdfb7aEI,13563
|
|
7
|
+
tree_annotated_plot/cli.py,sha256=Q8dtm3vWmAOJYwu4mrlrYsO0Ys59HYCyKBtBHIryvKo,11804
|
|
8
|
+
tree_annotated_plot-0.5.0.dist-info/METADATA,sha256=WyWmNl77MJQBDk8yWZiJJfvSMyaGu4S8O0qt2tpbICQ,11462
|
|
9
|
+
tree_annotated_plot-0.5.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
10
|
+
tree_annotated_plot-0.5.0.dist-info/entry_points.txt,sha256=K7edTocRbwmtM2OeZmCy3DLjmY3MR-q2EJmQQq4rj0E,69
|
|
11
|
+
tree_annotated_plot-0.5.0.dist-info/licenses/LICENSE,sha256=UIp30ue1HZit7tMmSK0SS3swJBqOcLLnLJn5LY5YdNE,1036
|
|
12
|
+
tree_annotated_plot-0.5.0.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
tree_annotated_plot/__init__.py,sha256=R39gjVnl3K2VjLba6zwtwPGADFmtNOE2UYNk1y9_z6k,351
|
|
2
|
-
tree_annotated_plot/_color.py,sha256=nOZp-KIPy6s-6gBkzW36oHrwDk_SmvVNsMiIJ-dY1Io,26519
|
|
3
|
-
tree_annotated_plot/_config.py,sha256=0ib600JaMpMz4ZjFYtvFu3nhUhliJRexTJcFpDKVOD0,13171
|
|
4
|
-
tree_annotated_plot/_plot.py,sha256=pqhR9PgQUML2-taP_k1FEidpIdBgJ6tulMEpFJbD8WI,77969
|
|
5
|
-
tree_annotated_plot/_tree.py,sha256=Bj-ABt8aooSwV7wAVTbwFGMmCnTPGOKweUWAdfb7aEI,13563
|
|
6
|
-
tree_annotated_plot/cli.py,sha256=OG6Qmzk5BaHUNrfOopX20FbzBN2ahr_SBxHpaCk6Z5c,9848
|
|
7
|
-
tree_annotated_plot-0.4.0.dist-info/METADATA,sha256=JQNY2-mmZIQA7TjABRX_YkvoZXxqix-Ixs5n23EQ1FY,11462
|
|
8
|
-
tree_annotated_plot-0.4.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
9
|
-
tree_annotated_plot-0.4.0.dist-info/entry_points.txt,sha256=K7edTocRbwmtM2OeZmCy3DLjmY3MR-q2EJmQQq4rj0E,69
|
|
10
|
-
tree_annotated_plot-0.4.0.dist-info/licenses/LICENSE,sha256=UIp30ue1HZit7tMmSK0SS3swJBqOcLLnLJn5LY5YdNE,1036
|
|
11
|
-
tree_annotated_plot-0.4.0.dist-info/RECORD,,
|
{tree_annotated_plot-0.4.0.dist-info → tree_annotated_plot-0.5.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{tree_annotated_plot-0.4.0.dist-info → tree_annotated_plot-0.5.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|