tree-annotated-plot 0.0.1__tar.gz → 0.2.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.
Files changed (43) hide show
  1. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/.github/workflows/release.yml +31 -1
  2. tree_annotated_plot-0.0.1/.github/workflows/ci.yml → tree_annotated_plot-0.2.0/.github/workflows/tests.yml +7 -1
  3. tree_annotated_plot-0.2.0/CHANGELOG.md +52 -0
  4. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/CLAUDE.md +13 -6
  5. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/PKG-INFO +32 -5
  6. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/README.md +31 -4
  7. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/docs/examples.md +166 -2
  8. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/docs/index.md +25 -0
  9. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/pyproject.toml +1 -1
  10. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/scripts/generate_docs_assets.py +103 -3
  11. tree_annotated_plot-0.2.0/src/tree_annotated_plot/_color.py +526 -0
  12. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/src/tree_annotated_plot/_config.py +99 -6
  13. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/src/tree_annotated_plot/_plot.py +361 -41
  14. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/src/tree_annotated_plot/_tree.py +83 -10
  15. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/src/tree_annotated_plot/cli.py +131 -0
  16. tree_annotated_plot-0.2.0/tests/test_appearance_tuning.py +797 -0
  17. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/tests/test_cli.py +4 -0
  18. tree_annotated_plot-0.2.0/tests/test_color_tree.py +800 -0
  19. tree_annotated_plot-0.2.0/tests/test_label_connection.py +559 -0
  20. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/tests/test_style.py +7 -2
  21. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/tests/test_tree_location.py +5 -0
  22. tree_annotated_plot-0.0.1/NEXT_STEPS.md +0 -94
  23. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/.github/workflows/docs.yml +0 -0
  24. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/.gitignore +0 -0
  25. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/LICENSE +0 -0
  26. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/docs/cli.md +0 -0
  27. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/docs/python-api.md +0 -0
  28. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/examples/fetch_auspice_data.py +0 -0
  29. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/examples/flu-seqneut-2025to2026_titer_charts.py +0 -0
  30. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/examples/synthetic_example.py +0 -0
  31. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/mkdocs.yml +0 -0
  32. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/scripts/build_docs.sh +0 -0
  33. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/scripts/check.sh +0 -0
  34. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/src/tree_annotated_plot/__init__.py +0 -0
  35. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/tests/test_branch_length.py +0 -0
  36. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/tests/test_chart_loader.py +0 -0
  37. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/tests/test_examples_run.py +0 -0
  38. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/tests/test_introspection.py +0 -0
  39. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/tests/test_plot.py +0 -0
  40. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/tests/test_plot_config.py +0 -0
  41. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/tests/test_real_data.py +0 -0
  42. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/tests/test_reconciliation.py +0 -0
  43. {tree_annotated_plot-0.0.1 → tree_annotated_plot-0.2.0}/tests/test_scale_bar.py +0 -0
@@ -1,4 +1,6 @@
1
- # Build wheel + sdist and publish to PyPI when a `v*` tag is pushed.
1
+ # When a `v*` tag is pushed: build wheel + sdist, publish to PyPI,
2
+ # then create a GitHub Release with auto-generated notes and the
3
+ # wheel + sdist attached as downloadable assets.
2
4
  #
3
5
  # Uses PyPI trusted publishing (OIDC) — no API token / password is
4
6
  # stored in the repo. The trusted-publisher relationship must be
@@ -80,3 +82,31 @@ jobs:
80
82
  path: dist/
81
83
 
82
84
  - uses: pypa/gh-action-pypi-publish@release/v1
85
+
86
+ github-release:
87
+ # Runs only after PyPI publish succeeds, so a transient PyPI failure
88
+ # doesn't leave a GitHub Release pointing at a version that isn't on
89
+ # PyPI. The Release page links to the tag, the wheel + sdist, and an
90
+ # auto-generated changelog (PR titles + commit list since the
91
+ # previous tag).
92
+ needs: publish
93
+ runs-on: ubuntu-latest
94
+ permissions:
95
+ # `softprops/action-gh-release` needs write access to create a
96
+ # Release on the tag and upload the dist files as assets.
97
+ contents: write
98
+ steps:
99
+ - uses: actions/download-artifact@v8
100
+ with:
101
+ name: dist
102
+ path: dist/
103
+
104
+ - uses: softprops/action-gh-release@v3
105
+ with:
106
+ # Attach the same wheel + sdist that went to PyPI so the
107
+ # Release page is a complete record of what was published.
108
+ files: dist/*
109
+ # Auto-generate notes from PR titles + commits since the
110
+ # previous tag. Maintainer can edit them after the fact in
111
+ # the Releases UI without re-running the workflow.
112
+ generate_release_notes: true
@@ -1,10 +1,16 @@
1
1
  # Run lint, format-check, and tests on every push to main and on every PR.
2
2
  #
3
+ # The workflow `name` is "tests" because that's what the README badge
4
+ # displays — readers (especially the biology audience) parse "tests
5
+ # passing" more readily than "CI passing". Behaviorally, this workflow
6
+ # also runs ruff and black (i.e. it's the project's full CI gate, not
7
+ # only tests); the slight label overload is intentional.
8
+ #
3
9
  # Local equivalent: `scripts/check.sh` (which expects a local .venv;
4
10
  # this workflow uses the runner's python directly so it doesn't depend
5
11
  # on that layout).
6
12
 
7
- name: ci
13
+ name: tests
8
14
 
9
15
  on:
10
16
  push:
@@ -0,0 +1,52 @@
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.0] - 2026-05-06
9
+
10
+ ### Added
11
+
12
+ - `connect_leader_to_label` (default `False`): extends each strain's
13
+ dashed leader line all the way to its text label. When on, the
14
+ chart's strain-axis labels are suppressed and replacement labels
15
+ are rendered alongside the tree on its chart-facing edge.
16
+ - `strain_label_font_size` (default `10`), `strain_label_font_weight`
17
+ (default `"normal"`), and `shift_tree_loc` (default `0`) for tuning
18
+ the size, weight, and placement of the connected labels.
19
+ - `color_tree_by` (default `None`): color the tree's branches and tip
20
+ circles by an Auspice node attribute (e.g. `"subclade"`) or by the
21
+ inferred genotype state at one or more sites
22
+ (e.g. `"genotype:HA1:158"` or `"genotype:HA1:158,189"`). Colors,
23
+ category ordering, and the bottom-of-plot legend match the
24
+ Nextstrain view of the same tree.
25
+ - `tree_color_scale` (default `None`): override the default coloring
26
+ with an explicit `{category: color}` mapping. Keys must match the
27
+ tree's categories one-to-one and the legend order follows the
28
+ user's key order. CLI form: `"value1=#hex1,value2=#hex2,..."`.
29
+ - `tree_color_legend_format` (default `None`): pass any subset of
30
+ Vega-Lite's
31
+ [Legend properties](https://vega.github.io/vega-lite/docs/legend.html#properties)
32
+ as a dict to style the tree's color legend (`orient`, `direction`,
33
+ `columns`, `padding`, `labelFontSize`, `titleFontSize`, …). When
34
+ `orient` is `"left"` or `"right"` and the user has not set
35
+ `columns` or `direction`, `columns=1` is forced so entries stack
36
+ vertically. CLI form: a JSON object string.
37
+ - `tree_color_legend_show` (default `True`): set to `False` to hide
38
+ the tree's color legend entirely while still coloring the tree.
39
+ - `scale_bar_font_size` (default `10`): font size for the tree's
40
+ scale bar label.
41
+
42
+ ### Changed
43
+
44
+ - Default `tree_line_width` bumped from `1.5` to `2`, default
45
+ `tree_node_size` from `28` to `45`. Tree branch lines and tip
46
+ circles are now drawn at full opacity. The thicker / fuller
47
+ defaults read better when the tree is colored (the prior values
48
+ were tuned for unicolor black trees).
49
+
50
+ ## [0.1.0] - 2026-05-04
51
+
52
+ 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`.
@@ -57,7 +62,7 @@
57
62
  scripts/check.sh
58
63
  ```
59
64
  The same three checks run in CI via
60
- [`.github/workflows/ci.yml`](.github/workflows/ci.yml) on push to
65
+ [`.github/workflows/tests.yml`](.github/workflows/tests.yml) on push to
61
66
  `main` and on PRs (CI also fetches the upstream Kikawa Auspice JSONs
62
67
  so real-data tests run rather than skip). Local green ≈ CI green.
63
68
  - **Build docs alongside checks** when changing public API surface:
@@ -95,11 +100,13 @@
95
100
  - **Releases are tag-driven**. Push a `vX.Y.Z` tag (after bumping
96
101
  `pyproject.toml`'s `version`) and
97
102
  [`.github/workflows/release.yml`](.github/workflows/release.yml)
98
- builds wheel + sdist and publishes to PyPI via trusted publishing
99
- (OIDC — no token in the repo). The build job verifies the tag and
100
- `pyproject.toml` version match before publishing. Per-release recipe
101
- and one-time PyPI configuration steps are in `README.md` under
102
- "Releasing a new version".
103
+ builds wheel + sdist, publishes to PyPI via trusted publishing
104
+ (OIDC — no token in the repo), and creates a matching GitHub
105
+ Release with auto-generated notes + the wheel/sdist attached as
106
+ assets. The build job verifies the tag and `pyproject.toml` version
107
+ match before publishing. Per-release recipe and one-time PyPI
108
+ configuration steps are in `README.md` under "Releasing a new
109
+ version".
103
110
  - **Adding a new example** (full recipe in `README.md`):
104
111
  1. Self-contained module under `examples/` with module-level
105
112
  helpers (callable from outside).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tree-annotated-plot
3
- Version: 0.0.1
3
+ Version: 0.2.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
@@ -51,6 +51,10 @@ Description-Content-Type: text/markdown
51
51
 
52
52
  # `tree-annotated-plot`: annotate the axis of an Altair / Vega-Lite plot with a phylogenetic tree
53
53
 
54
+ [![PyPI version](https://img.shields.io/pypi/v/tree-annotated-plot.svg)](https://pypi.org/project/tree-annotated-plot/)
55
+ [![Python versions](https://img.shields.io/pypi/pyversions/tree-annotated-plot.svg)](https://pypi.org/project/tree-annotated-plot/)
56
+ [![tests](https://github.com/jbloomlab/tree-annotated-plot/actions/workflows/tests.yml/badge.svg)](https://github.com/jbloomlab/tree-annotated-plot/actions/workflows/tests.yml)
57
+ [![Docs](https://img.shields.io/github/deployments/jbloomlab/tree-annotated-plot/github-pages?label=docs)](https://jbloomlab.github.io/tree-annotated-plot/)
54
58
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
55
59
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
56
60
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
@@ -60,6 +64,26 @@ See [https://jbloomlab.github.io/tree-annotated-plot/](https://jbloomlab.github.
60
64
 
61
65
  > **Note:** charts must be saved from altair 6+ (Vega-Lite v6). Older specs raise by default; pass `--no-strict-version` (CLI) or `strict_version=False` (Python) to override at your own risk. See the [docs](https://jbloomlab.github.io/tree-annotated-plot/) for details.
62
66
 
67
+ ## Installation
68
+
69
+ Released on [PyPI](https://pypi.org/project/tree-annotated-plot/). Requires Python 3.13+.
70
+
71
+ ```bash
72
+ pip install tree-annotated-plot
73
+ ```
74
+
75
+ To install the bleeding edge directly from this repository:
76
+
77
+ ```bash
78
+ pip install git+https://github.com/jbloomlab/tree-annotated-plot.git
79
+ ```
80
+
81
+ For a development checkout, see [Installation (development)](#installation-development) below.
82
+
83
+ ## Changelog
84
+
85
+ See [CHANGELOG.md](CHANGELOG.md) for the version history.
86
+
63
87
  ## Notes for developing the package
64
88
 
65
89
  ### Installation (development)
@@ -110,9 +134,9 @@ Then open `site/examples.html` in a browser and, for each example:
110
134
  If something visibly regresses, fix it before pushing — there's no
111
135
  CI gate that will catch a visual regression for you.
112
136
 
113
- ### Continuous integration
137
+ ### Continuous integration tests
114
138
 
115
- [`.github/workflows/ci.yml`](.github/workflows/ci.yml) runs on every
139
+ [`.github/workflows/tests.yml`](.github/workflows/tests.yml) runs on every
116
140
  push to `main` and every pull request: it installs the package, fetches
117
141
  the upstream Kikawa Auspice JSONs (so the real-data tests actually
118
142
  exercise rather than skip), builds the Kikawa titer chart specs, and
@@ -129,8 +153,11 @@ merge), and additionally deploys `site/` to GitHub Pages on push to
129
153
 
130
154
  A third workflow,
131
155
  [`.github/workflows/release.yml`](.github/workflows/release.yml),
132
- builds the sdist + wheel and publishes them to PyPI when a `v*` tag
133
- is pushed (see "Releasing a new version" below).
156
+ builds the sdist + wheel when a `v*` tag is pushed, publishes them
157
+ to PyPI, and creates a matching
158
+ [GitHub Release](https://github.com/jbloomlab/tree-annotated-plot/releases)
159
+ with auto-generated notes and the wheel/sdist attached (see
160
+ "Releasing a new version" below).
134
161
 
135
162
  ### Releasing a new version
136
163
 
@@ -1,5 +1,9 @@
1
1
  # `tree-annotated-plot`: annotate the axis of an Altair / Vega-Lite plot with a phylogenetic tree
2
2
 
3
+ [![PyPI version](https://img.shields.io/pypi/v/tree-annotated-plot.svg)](https://pypi.org/project/tree-annotated-plot/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/tree-annotated-plot.svg)](https://pypi.org/project/tree-annotated-plot/)
5
+ [![tests](https://github.com/jbloomlab/tree-annotated-plot/actions/workflows/tests.yml/badge.svg)](https://github.com/jbloomlab/tree-annotated-plot/actions/workflows/tests.yml)
6
+ [![Docs](https://img.shields.io/github/deployments/jbloomlab/tree-annotated-plot/github-pages?label=docs)](https://jbloomlab.github.io/tree-annotated-plot/)
3
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
8
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
5
9
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
@@ -9,6 +13,26 @@ See [https://jbloomlab.github.io/tree-annotated-plot/](https://jbloomlab.github.
9
13
 
10
14
  > **Note:** charts must be saved from altair 6+ (Vega-Lite v6). Older specs raise by default; pass `--no-strict-version` (CLI) or `strict_version=False` (Python) to override at your own risk. See the [docs](https://jbloomlab.github.io/tree-annotated-plot/) for details.
11
15
 
16
+ ## Installation
17
+
18
+ Released on [PyPI](https://pypi.org/project/tree-annotated-plot/). Requires Python 3.13+.
19
+
20
+ ```bash
21
+ pip install tree-annotated-plot
22
+ ```
23
+
24
+ To install the bleeding edge directly from this repository:
25
+
26
+ ```bash
27
+ pip install git+https://github.com/jbloomlab/tree-annotated-plot.git
28
+ ```
29
+
30
+ For a development checkout, see [Installation (development)](#installation-development) below.
31
+
32
+ ## Changelog
33
+
34
+ See [CHANGELOG.md](CHANGELOG.md) for the version history.
35
+
12
36
  ## Notes for developing the package
13
37
 
14
38
  ### Installation (development)
@@ -59,9 +83,9 @@ Then open `site/examples.html` in a browser and, for each example:
59
83
  If something visibly regresses, fix it before pushing — there's no
60
84
  CI gate that will catch a visual regression for you.
61
85
 
62
- ### Continuous integration
86
+ ### Continuous integration tests
63
87
 
64
- [`.github/workflows/ci.yml`](.github/workflows/ci.yml) runs on every
88
+ [`.github/workflows/tests.yml`](.github/workflows/tests.yml) runs on every
65
89
  push to `main` and every pull request: it installs the package, fetches
66
90
  the upstream Kikawa Auspice JSONs (so the real-data tests actually
67
91
  exercise rather than skip), builds the Kikawa titer chart specs, and
@@ -78,8 +102,11 @@ merge), and additionally deploys `site/` to GitHub Pages on push to
78
102
 
79
103
  A third workflow,
80
104
  [`.github/workflows/release.yml`](.github/workflows/release.yml),
81
- builds the sdist + wheel and publishes them to PyPI when a `v*` tag
82
- is pushed (see "Releasing a new version" below).
105
+ builds the sdist + wheel when a `v*` tag is pushed, publishes them
106
+ to PyPI, and creates a matching
107
+ [GitHub Release](https://github.com/jbloomlab/tree-annotated-plot/releases)
108
+ with auto-generated notes and the wheel/sdist attached (see
109
+ "Releasing a new version" below).
83
110
 
84
111
  ### Releasing a new version
85
112
 
@@ -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
 
@@ -24,6 +24,31 @@ with `Chart` / `LayerChart` / `FacetChart` / `HConcatChart` /
24
24
  axis. The tree drops to whichever side of the chart the strain labels
25
25
  sit on by default (left for y-axis, bottom for x-axis).
26
26
 
27
+ ## Installation
28
+
29
+ Released on [PyPI](https://pypi.org/project/tree-annotated-plot/).
30
+ Requires Python 3.13+.
31
+
32
+ ```bash
33
+ pip install tree-annotated-plot
34
+ ```
35
+
36
+ To pin a specific version:
37
+
38
+ ```bash
39
+ pip install tree-annotated-plot==0.0.1
40
+ ```
41
+
42
+ To install the bleeding edge directly from the
43
+ [GitHub source](https://github.com/jbloomlab/tree-annotated-plot):
44
+
45
+ ```bash
46
+ pip install git+https://github.com/jbloomlab/tree-annotated-plot.git
47
+ ```
48
+
49
+ For a development checkout (contributors), see the README's
50
+ [development install](https://github.com/jbloomlab/tree-annotated-plot#installation-development).
51
+
27
52
  ## 30-second example
28
53
 
29
54
  Given an Auspice tree at `h3n2.auspice.json` and a Vega-Lite chart spec
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "tree-annotated-plot"
7
- version = "0.0.1"
7
+ version = "0.2.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.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/`."""