tree-annotated-plot 0.1.0__tar.gz → 0.2.1__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.
Files changed (42) hide show
  1. tree_annotated_plot-0.2.1/CHANGELOG.md +65 -0
  2. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/CLAUDE.md +5 -0
  3. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/PKG-INFO +5 -1
  4. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/README.md +4 -0
  5. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/docs/examples.md +166 -2
  6. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/pyproject.toml +1 -1
  7. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/scripts/generate_docs_assets.py +103 -3
  8. tree_annotated_plot-0.2.1/src/tree_annotated_plot/_color.py +526 -0
  9. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/src/tree_annotated_plot/_config.py +99 -6
  10. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/src/tree_annotated_plot/_plot.py +451 -79
  11. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/src/tree_annotated_plot/_tree.py +83 -10
  12. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/src/tree_annotated_plot/cli.py +131 -0
  13. tree_annotated_plot-0.2.1/tests/test_appearance_tuning.py +797 -0
  14. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/tests/test_cli.py +4 -0
  15. tree_annotated_plot-0.2.1/tests/test_color_tree.py +800 -0
  16. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/tests/test_introspection.py +68 -1
  17. tree_annotated_plot-0.2.1/tests/test_label_connection.py +559 -0
  18. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/tests/test_plot.py +67 -0
  19. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/tests/test_style.py +7 -2
  20. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/tests/test_tree_location.py +5 -0
  21. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/.github/workflows/docs.yml +0 -0
  22. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/.github/workflows/release.yml +0 -0
  23. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/.github/workflows/tests.yml +0 -0
  24. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/.gitignore +0 -0
  25. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/LICENSE +0 -0
  26. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/docs/cli.md +0 -0
  27. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/docs/index.md +0 -0
  28. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/docs/python-api.md +0 -0
  29. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/examples/fetch_auspice_data.py +0 -0
  30. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/examples/flu-seqneut-2025to2026_titer_charts.py +0 -0
  31. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/examples/synthetic_example.py +0 -0
  32. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/mkdocs.yml +0 -0
  33. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/scripts/build_docs.sh +0 -0
  34. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/scripts/check.sh +0 -0
  35. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/src/tree_annotated_plot/__init__.py +0 -0
  36. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/tests/test_branch_length.py +0 -0
  37. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/tests/test_chart_loader.py +0 -0
  38. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/tests/test_examples_run.py +0 -0
  39. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/tests/test_plot_config.py +0 -0
  40. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/tests/test_real_data.py +0 -0
  41. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/tests/test_reconciliation.py +0 -0
  42. {tree_annotated_plot-0.1.0 → tree_annotated_plot-0.2.1}/tests/test_scale_bar.py +0 -0
@@ -0,0 +1,65 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.2.1] - 2026-05-06
9
+
10
+ ### Fixed
11
+
12
+ - Apply tree-tip ordering to chart axes that use untyped Altair
13
+ shorthand (`alt.Y("strain")` without a `:N` / `:O` type suffix).
14
+ Previously the sort override was silently skipped, so the chart
15
+ rendered in data order instead of tree order.
16
+ - An internal consistency check now raises if the spec-level walk
17
+ and the live-object walk over the user's chart ever disagree on
18
+ the number of strain-axis encodings, so silent skips like the
19
+ one above can't recur in another shape.
20
+
21
+ ## [0.2.0] - 2026-05-06
22
+
23
+ ### Added
24
+
25
+ - `connect_leader_to_label` (default `False`): extends each strain's
26
+ dashed leader line all the way to its text label. When on, the
27
+ chart's strain-axis labels are suppressed and replacement labels
28
+ are rendered alongside the tree on its chart-facing edge.
29
+ - `strain_label_font_size` (default `10`), `strain_label_font_weight`
30
+ (default `"normal"`), and `shift_tree_loc` (default `0`) for tuning
31
+ the size, weight, and placement of the connected labels.
32
+ - `color_tree_by` (default `None`): color the tree's branches and tip
33
+ circles by an Auspice node attribute (e.g. `"subclade"`) or by the
34
+ inferred genotype state at one or more sites
35
+ (e.g. `"genotype:HA1:158"` or `"genotype:HA1:158,189"`). Colors,
36
+ category ordering, and the bottom-of-plot legend match the
37
+ Nextstrain view of the same tree.
38
+ - `tree_color_scale` (default `None`): override the default coloring
39
+ with an explicit `{category: color}` mapping. Keys must match the
40
+ tree's categories one-to-one and the legend order follows the
41
+ user's key order. CLI form: `"value1=#hex1,value2=#hex2,..."`.
42
+ - `tree_color_legend_format` (default `None`): pass any subset of
43
+ Vega-Lite's
44
+ [Legend properties](https://vega.github.io/vega-lite/docs/legend.html#properties)
45
+ as a dict to style the tree's color legend (`orient`, `direction`,
46
+ `columns`, `padding`, `labelFontSize`, `titleFontSize`, …). When
47
+ `orient` is `"left"` or `"right"` and the user has not set
48
+ `columns` or `direction`, `columns=1` is forced so entries stack
49
+ vertically. CLI form: a JSON object string.
50
+ - `tree_color_legend_show` (default `True`): set to `False` to hide
51
+ the tree's color legend entirely while still coloring the tree.
52
+ - `scale_bar_font_size` (default `10`): font size for the tree's
53
+ scale bar label.
54
+
55
+ ### Changed
56
+
57
+ - Default `tree_line_width` bumped from `1.5` to `2`, default
58
+ `tree_node_size` from `28` to `45`. Tree branch lines and tip
59
+ circles are now drawn at full opacity. The thicker / fuller
60
+ defaults read better when the tree is colored (the prior values
61
+ were tuned for unicolor black trees).
62
+
63
+ ## [0.1.0] - 2026-05-04
64
+
65
+ Initial release.
@@ -3,6 +3,11 @@
3
3
  - **Keep CLAUDE.md, README.md, and the docs site updated** when changing the
4
4
  code. `CLAUDE.md` describes programming conventions; `README.md` describes
5
5
  basic use; `docs/` is the user-facing reference. Don't let them drift.
6
+ - **Record user-facing changes in `CHANGELOG.md`** under the `## [Unreleased]`
7
+ section as you make them — new parameters, behavior changes, removed
8
+ features, bug fixes — in [Keep a Changelog](https://keepachangelog.com/)
9
+ format. At release time, `## [Unreleased]` is renamed to
10
+ `## [X.Y.Z] - YYYY-MM-DD` and a fresh `## [Unreleased]` is created.
6
11
  - **Single source of truth — `pyproject.toml`**: canonical for dependencies,
7
12
  supported Python version, build config, tool settings. Don't restate any of
8
13
  these in prose; refer to `pyproject.toml`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tree-annotated-plot
3
- Version: 0.1.0
3
+ Version: 0.2.1
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
@@ -80,6 +80,10 @@ pip install git+https://github.com/jbloomlab/tree-annotated-plot.git
80
80
 
81
81
  For a development checkout, see [Installation (development)](#installation-development) below.
82
82
 
83
+ ## Changelog
84
+
85
+ See [CHANGELOG.md](CHANGELOG.md) for the version history.
86
+
83
87
  ## Notes for developing the package
84
88
 
85
89
  ### Installation (development)
@@ -29,6 +29,10 @@ pip install git+https://github.com/jbloomlab/tree-annotated-plot.git
29
29
 
30
30
  For a development checkout, see [Installation (development)](#installation-development) below.
31
31
 
32
+ ## Changelog
33
+
34
+ See [CHANGELOG.md](CHANGELOG.md) for the version history.
35
+
32
36
  ## Notes for developing the package
33
37
 
34
38
  ### Installation (development)
@@ -97,8 +97,170 @@ With the tree panel added by `tree-annotated-plot`:
97
97
  The strain axis is on `y`, so `tree_annotated_plot.plot` auto-picks
98
98
  the **vertical layout** (`tree_location` defaults to `"left"` on a
99
99
  y-encoded strain): result is an `HConcatChart` with the tree on the
100
- left, tips flush against the chart's strain labels, and a centered
101
- scale bar at the bottom of the tree panel.
100
+ left and a centered scale bar at the bottom of the tree panel. The
101
+ chart's natural strain-axis labels are kept exactly as the
102
+ chart-builder wrote them (fonts, ticks, axis title, and all), and the
103
+ tree's dashed leader lines stop at the tree panel's chart-facing
104
+ edge.
105
+
106
+ The H3N2 example above is rendered with `color_tree_by="subclade"`,
107
+ which colors the tree's branches and tip circles by the
108
+ `node_attrs.subclade` value at each node and adds a categorical legend
109
+ below the plot. See "Color the tree" below for the full set of options.
110
+
111
+ ### Optional: connect leaders all the way to the labels
112
+
113
+ If you'd prefer the dashed leaders to run flush into the strain
114
+ labels themselves (with no break between tip and label), set
115
+ `connect_leader_to_label=True`. This involves moving the labels off
116
+ the chart's natural axis and into the tree panel, with a few
117
+ trade-offs to be aware of:
118
+
119
+ - The chart's strain-axis is replaced: any labels, ticks, title,
120
+ or custom `axis=...` you set on that encoding are dropped, and
121
+ replacement labels are rendered alongside the tree.
122
+ - Label widths are estimated, not measured exactly, so layout may
123
+ need tuning. The two main knobs are `strain_label_font_size`
124
+ (default 10) and `shift_tree_loc` (a manual pixel offset that
125
+ moves the tree closer to the labels).
126
+
127
+ The example below turns on label connection, shrinks the labels to
128
+ 9 pt, and uses `shift_tree_loc=60` to bring the tree flush against
129
+ them:
130
+
131
+ ![H3N2 with label connection at 9pt font](images/h3n2_combined_label_connect.svg)
132
+
133
+ [Open the interactive chart in a new tab →](charts/h3n2_combined_label_connect.html){target="_blank"}
134
+
135
+ CLI flags: `--connect-leader-to-label --strain-label-font-size 9
136
+ --shift-tree-loc 60`. In Python:
137
+ `connect_leader_to_label=True, strain_label_font_size=9, shift_tree_loc=60`.
138
+
139
+ ### Color the tree
140
+
141
+ Pass `color_tree_by` to color the tree's branches and tip circles by
142
+ any property the Auspice JSON exposes — broadly, anything that
143
+ appears in the "Color By" dropdown on the Nextstrain view of the same
144
+ tree. Two forms are supported:
145
+
146
+ - A named attribute, e.g. `color_tree_by="subclade"` (used in the
147
+ example above). Common alternatives include `"clade_membership"`,
148
+ `"region"`, `"country"` — whichever the tree provides.
149
+ - A genotype at one or more sites in a gene. For a single site,
150
+ `color_tree_by="genotype:HA1:158"` colors each tip by the amino
151
+ acid at HA1 site 158. A comma-separated list gives a haplotype:
152
+ `"genotype:HA1:158,189"`. Sites that don't vary in the tree are
153
+ dropped from the haplotype label.
154
+
155
+ Colors match what you'd see on the Nextstrain view of the same tree —
156
+ either from the JSON's palette information when the build provides it,
157
+ or from the same default palette Auspice uses when it doesn't.
158
+ Categories are ordered by descending frequency in both cases. Missing
159
+ values render in gray, and the legend is drawn at the bottom of the
160
+ combined plot.
161
+
162
+ The example below colors the same H3N2 chart by genotype at HA1
163
+ site 158, which has two mutations in the tree (`N158K`, `N158D`) and
164
+ so renders three states (N, K, D):
165
+
166
+ ![H3N2 combined chart, colored by genotype HA1:158](images/h3n2_combined_genotype_158.svg)
167
+
168
+ [Open the interactive chart in a new tab →](charts/h3n2_combined_genotype_158.html){target="_blank"}
169
+
170
+ CLI flag: `--color-tree-by genotype:HA1:158`. In Python:
171
+ `color_tree_by="genotype:HA1:158"`.
172
+
173
+ ### Customizing the colors and legend
174
+
175
+ Four optional knobs let you override the defaults:
176
+
177
+ - `tree_color_scale` — supply your own `{category: color}` mapping
178
+ instead of the Auspice palette. Keys must match the tree's
179
+ categories one-to-one (extras or misses are an error). The legend
180
+ order follows the order of keys you pass. `"unknown"` is always
181
+ gray and must not be specified.
182
+ - `tree_color_legend_format` — pass any subset of Vega-Lite's
183
+ [Legend properties](https://vega.github.io/vega-lite/docs/legend.html#properties)
184
+ as a dict to style the legend (`orient`, `direction`, `columns`,
185
+ `padding`, `offset`, `labelFontSize`, `titleFontSize`, …). Smart
186
+ default: when `orient` is `"left"` or `"right"` and you have not
187
+ set `columns` or `direction`, entries stack vertically
188
+ (`columns=1` is forced).
189
+ - `tree_color_legend_show` — set to `False` to hide the legend
190
+ entirely while keeping the tree colored.
191
+ - `scale_bar_font_size` — font size for the scale bar's label.
192
+
193
+ The exact category strings depend on the form of `color_tree_by`:
194
+
195
+ | `color_tree_by` | Example category strings |
196
+ |---|---|
197
+ | node attribute (e.g. `"subclade"`) | `"K"`, `"J.2"`, `"J.2.4"` |
198
+ | `"genotype:HA1:158"` (single site) | `"K158"`, `"R158"`, `"E158"` |
199
+ | `"genotype:HA1:158,189"` (both sites vary) | `"K158/E189"`, `"R158/E189"` |
200
+ | `"genotype:HA1:158,189"` (189 invariant) | `"K158"`, `"R158"` (invariant site dropped) |
201
+
202
+ If you pass keys that don't match, the error message lists the
203
+ tree's actual categories so you can copy them.
204
+
205
+ The H3N2 example below puts all four knobs to work: an explicit
206
+ 6-color palette (a colorblind-safe Okabe–Ito-inspired set) ordered
207
+ K → J.2.4 → J.2.3 → J.2.2 → J.2 → G.1.3.1, the legend moved to the
208
+ **left** of the combined plot at 14-pt, and a matching 14-pt
209
+ scale-bar label.
210
+
211
+ ![H3N2 combined chart with custom colors and legend](images/h3n2_combined_custom_colors.svg)
212
+
213
+ [Open the interactive chart in a new tab →](charts/h3n2_combined_custom_colors.html){target="_blank"}
214
+
215
+ ```python
216
+ out = tree_annotated_plot.plot(
217
+ "examples/data/flu-seqneut-2025to2026_H3N2.json",
218
+ chart,
219
+ chart_strain_field="axis_label",
220
+ tree_strain_field="derived_haplotype",
221
+ branch_length="div",
222
+ tree_size=140,
223
+ scale_bar=True,
224
+ branch_length_units="substitutions",
225
+ color_tree_by="subclade",
226
+ tree_color_scale={
227
+ "K": "#0072B2",
228
+ "J.2.4": "#009E73",
229
+ "J.2.3": "#D55E00",
230
+ "J.2.2": "#CC79A7",
231
+ "J.2": "#56B4E9",
232
+ "G.1.3.1": "#E69F00",
233
+ },
234
+ tree_color_legend_format={
235
+ "orient": "left",
236
+ "labelFontSize": 14,
237
+ "titleFontSize": 14,
238
+ },
239
+ scale_bar_font_size=14,
240
+ )
241
+ ```
242
+
243
+ `--tree-color-scale` on the CLI is a comma-separated list of
244
+ `key=color` pairs and `--tree-color-legend-format` is a JSON object
245
+ string (quote the whole argument in both cases so the shell doesn't
246
+ interpret `#`, braces, or quotes):
247
+
248
+ ```bash
249
+ tree-annotated-plot \
250
+ --tree examples/data/flu-seqneut-2025to2026_H3N2.json \
251
+ --chart examples/data/flu-seqneut-2025to2026_H3N2_titers.json \
252
+ --chart-strain-field axis_label \
253
+ --tree-strain-field derived_haplotype \
254
+ --branch-length div \
255
+ --tree-size 140 \
256
+ --scale-bar \
257
+ --branch-length-units substitutions \
258
+ --color-tree-by subclade \
259
+ --tree-color-scale "K=#0072B2,J.2.4=#009E73,J.2.3=#D55E00,J.2.2=#CC79A7,J.2=#56B4E9,G.1.3.1=#E69F00" \
260
+ --tree-color-legend-format '{"orient":"left","labelFontSize":14,"titleFontSize":14}' \
261
+ --scale-bar-font-size 14 \
262
+ --output examples/data/h3n2_combined_custom_colors.html
263
+ ```
102
264
 
103
265
  ### Reproduce — command line
104
266
 
@@ -114,6 +276,7 @@ tree-annotated-plot \
114
276
  --tree-size 140 \
115
277
  --scale-bar \
116
278
  --branch-length-units substitutions \
279
+ --color-tree-by subclade \
117
280
  --output examples/data/h3n2_combined.json
118
281
  ```
119
282
 
@@ -131,6 +294,7 @@ out = tree_annotated_plot.plot(
131
294
  tree_size=140,
132
295
  scale_bar=True,
133
296
  branch_length_units="substitutions",
297
+ color_tree_by="subclade",
134
298
  )
135
299
  ```
136
300
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "tree-annotated-plot"
7
- version = "0.1.0"
7
+ version = "0.2.1"
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.13"
@@ -110,9 +110,7 @@ def _render_kikawa() -> None:
110
110
  # Render the bare chart (no tree) so the docs page can show what
111
111
  # the chart looks like before tree-annotated-plot wraps it.
112
112
  _save_pair(chart, f"{basename}_chart_only")
113
- out = tree_annotated_plot.plot(
114
- DATA_DIR / f"flu-seqneut-2025to2026_{subtype}.json",
115
- chart,
113
+ plot_kwargs = dict(
116
114
  chart_strain_field="axis_label",
117
115
  tree_strain_field="derived_haplotype",
118
116
  branch_length="div",
@@ -120,8 +118,110 @@ def _render_kikawa() -> None:
120
118
  scale_bar=True,
121
119
  branch_length_units="substitutions",
122
120
  )
121
+ if subtype == "H3N2":
122
+ # Color H3N2 by subclade so the docs SVG matches what users see
123
+ # on Nextstrain. The Auspice JSON's meta.colorings.subclade has
124
+ # no `scale` defined, so colors come from the default palette.
125
+ plot_kwargs["color_tree_by"] = "subclade"
126
+ out = tree_annotated_plot.plot(
127
+ DATA_DIR / f"flu-seqneut-2025to2026_{subtype}.json",
128
+ chart,
129
+ **plot_kwargs,
130
+ )
123
131
  _save_pair(out, f"{basename}_combined")
124
132
 
133
+ # H3N2 again, with `connect_leader_to_label=True` and a 9-point label
134
+ # font: opt-in label connection where leaders run flush into the
135
+ # labels rendered alongside the tree (the default keeps the chart's
136
+ # natural strain-axis labels untouched).
137
+ h3n2_chart = builder.make_chart(
138
+ subtype="H3N2",
139
+ chart_type="iqr",
140
+ titers=titers,
141
+ viruses=viruses,
142
+ metadata=metadata,
143
+ all_cohorts=all_cohorts,
144
+ )
145
+ out = tree_annotated_plot.plot(
146
+ DATA_DIR / "flu-seqneut-2025to2026_H3N2.json",
147
+ h3n2_chart,
148
+ chart_strain_field="axis_label",
149
+ tree_strain_field="derived_haplotype",
150
+ branch_length="div",
151
+ tree_size=140,
152
+ scale_bar=True,
153
+ branch_length_units="substitutions",
154
+ connect_leader_to_label=True,
155
+ strain_label_font_size=9,
156
+ shift_tree_loc=60,
157
+ color_tree_by="subclade",
158
+ )
159
+ _save_pair(out, "h3n2_combined_label_connect")
160
+
161
+ # H3N2 once more, colored by genotype at HA1 site 158: same chart and
162
+ # default layout as `h3n2_combined`, with `color_tree_by` switched to
163
+ # the genotype form. Site 158 has two mutations (N158K, N158D) in the
164
+ # tree, so this renders three states (N, K, D).
165
+ h3n2_chart_genotype = builder.make_chart(
166
+ subtype="H3N2",
167
+ chart_type="iqr",
168
+ titers=titers,
169
+ viruses=viruses,
170
+ metadata=metadata,
171
+ all_cohorts=all_cohorts,
172
+ )
173
+ out = tree_annotated_plot.plot(
174
+ DATA_DIR / "flu-seqneut-2025to2026_H3N2.json",
175
+ h3n2_chart_genotype,
176
+ chart_strain_field="axis_label",
177
+ tree_strain_field="derived_haplotype",
178
+ branch_length="div",
179
+ tree_size=140,
180
+ scale_bar=True,
181
+ branch_length_units="substitutions",
182
+ color_tree_by="genotype:HA1:158",
183
+ )
184
+ _save_pair(out, "h3n2_combined_genotype_158")
185
+
186
+ # H3N2 once more, demonstrating all four appearance-tuning knobs:
187
+ # an explicit `tree_color_scale` (Okabe-Ito-inspired colorblind-safe
188
+ # palette, ordered K, J.2.4, J.2.3, J.2.2, J.2, G.1.3.1), a 14-pt
189
+ # legend on the left, and a 14-pt scale-bar label.
190
+ h3n2_chart_custom = builder.make_chart(
191
+ subtype="H3N2",
192
+ chart_type="iqr",
193
+ titers=titers,
194
+ viruses=viruses,
195
+ metadata=metadata,
196
+ all_cohorts=all_cohorts,
197
+ )
198
+ out = tree_annotated_plot.plot(
199
+ DATA_DIR / "flu-seqneut-2025to2026_H3N2.json",
200
+ h3n2_chart_custom,
201
+ chart_strain_field="axis_label",
202
+ tree_strain_field="derived_haplotype",
203
+ branch_length="div",
204
+ tree_size=140,
205
+ scale_bar=True,
206
+ branch_length_units="substitutions",
207
+ color_tree_by="subclade",
208
+ tree_color_scale={
209
+ "K": "#0072B2",
210
+ "J.2.4": "#009E73",
211
+ "J.2.3": "#D55E00",
212
+ "J.2.2": "#CC79A7",
213
+ "J.2": "#56B4E9",
214
+ "G.1.3.1": "#E69F00",
215
+ },
216
+ tree_color_legend_format={
217
+ "orient": "left",
218
+ "labelFontSize": 14,
219
+ "titleFontSize": 14,
220
+ },
221
+ scale_bar_font_size=14,
222
+ )
223
+ _save_pair(out, "h3n2_combined_custom_colors")
224
+
125
225
 
126
226
  def main() -> None:
127
227
  """Render every example to SVG + interactive HTML under `docs/`."""