dendrofan 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dendrofan contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,170 @@
1
+ Metadata-Version: 2.4
2
+ Name: dendrofan
3
+ Version: 0.1.1
4
+ Summary: Circular ("fan") dendrograms for Python, in the visual style of R's ape package
5
+ Author: Rafael Magallanes Quintanar
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/tiquis/dendrofan
8
+ Project-URL: Issues, https://github.com/tiquis/dendrofan/issues
9
+ Keywords: dendrogram,hierarchical-clustering,circular-plot,fan-plot,phylogenetics,data-visualization,scipy,matplotlib
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Topic :: Scientific/Engineering :: Visualization
16
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
17
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: numpy>=1.20
22
+ Requires-Dist: scipy>=1.7
23
+ Requires-Dist: matplotlib>=3.4
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7; extra == "dev"
26
+ Requires-Dist: pytest-mpl; extra == "dev"
27
+ Requires-Dist: build; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # dendrofan
31
+
32
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21251841.svg)](https://doi.org/10.5281/zenodo.21251841)
33
+
34
+ Circular ("fan") dendrograms for Python, in the visual style of R's
35
+ [`ape`](https://cran.r-project.org/package=ape) package
36
+ (`plot.phylo(type = "fan")`, Paradis & Schliep, 2019) — computed and
37
+ rendered entirely with NumPy, SciPy, and Matplotlib. No R dependency,
38
+ no reimplementation of clustering.
39
+
40
+ ## Why this exists
41
+
42
+ `scipy.cluster.hierarchy` has no circular/fan layout. The usual
43
+ workaround is a one-off script: call
44
+ `scipy.cluster.hierarchy.dendrogram(..., no_plot=True)`, take its
45
+ rectangular `icoord`/`dcoord` output, and manually re-project it into
46
+ polar coordinates with Matplotlib. That works for one figure, but it
47
+ tends to:
48
+
49
+ - assume a full 360° circle with no gap for labels at the seam,
50
+ - divide by `dcoord.max()` without checking for a degenerate,
51
+ all-zero-height tree,
52
+ - hardcode SciPy's 10-units-per-leaf spacing convention,
53
+ - have no story for trees with 1 or 2 leaves,
54
+ - mix clustering, geometry, and plotting into a single script, so
55
+ none of it is reusable for the next dataset.
56
+
57
+ dendrofan factors the actually-reusable idea — the rectangular-to-polar
58
+ coordinate transform — into a small, tested, documented library, and
59
+ builds a real plotting API on top of it.
60
+
61
+ ## Install
62
+
63
+ ```bash
64
+ pip install -e . # from a source checkout
65
+ pip install -e ".[dev]" # + pytest, for running the test suite
66
+ ```
67
+
68
+ ## Quick start
69
+
70
+ ```python
71
+ import numpy as np
72
+ from dendrofan import circular_dendrogram
73
+
74
+ rng = np.random.default_rng(0)
75
+ data = rng.normal(size=(20, 6))
76
+ labels = [f"sample_{i:02d}" for i in range(20)]
77
+
78
+ result = circular_dendrogram(
79
+ data,
80
+ labels=labels,
81
+ method="ward", # forwarded to scipy.cluster.hierarchy.linkage
82
+ span=350, # leave a small angular gap at the seam
83
+ inner_radius=0.15, # small central gap, ape's fan style
84
+ )
85
+ result.fig.savefig("tree.png", dpi=200, bbox_inches="tight")
86
+ ```
87
+
88
+ `circular_dendrogram` accepts the same three kinds of input as SciPy's
89
+ own `dendrogram`: raw observations (`data=...`), a precomputed linkage
90
+ matrix (`Z=...`), or a precomputed condensed distance vector
91
+ (`condensed_distances=...`).
92
+
93
+ See [`examples/quickstart.py`](examples/quickstart.py) and
94
+ [`examples/reproduce_station_dendrogram.py`](examples/reproduce_station_dendrogram.py)
95
+ (a fully worked, colored-by-group example) for more.
96
+
97
+ ## What it handles that the ad hoc version didn't
98
+
99
+ | Case | Ad hoc script | dendrofan |
100
+ |---|---|---|
101
+ | Angular gap at the seam (for label room) | not supported (full circle only) | `span=350` (or any value `<= 360`) |
102
+ | All-merge-heights-equal-zero tree | `ZeroDivisionError` / silent NaNs | falls back to placing all nodes at the leaf radius |
103
+ | 1 or 2 leaves | untested, breaks silently | validated; raises `DegenerateTreeError` for < 2, works for 2 |
104
+ | Mismatched label count | silent misalignment | raises `LabelMismatchError` |
105
+ | Invalid linkage matrix / distance vector | undefined behavior | raises `InvalidLinkageError` before plotting |
106
+ | Root at centre vs. rim, radius scale | hardcoded linear, root-at-centre | `inner_radius`/`outer_radius`/`invert_radius`, plus optional `"sqrt"` area-preserving radius scale |
107
+ | Per-clade coloring | manual, one-off | `color_threshold`/`link_color_func` forwarded to SciPy, or a `label_colors` dict/callable |
108
+ | Scale reference / clade highlighting | not present | `dendrofan.annotations.add_scale_ring`, `highlight_sector` |
109
+ | Reuse across datasets | copy-paste the script | one function call |
110
+
111
+ ## API overview
112
+
113
+ - `dendrofan.circular_dendrogram(...)` — the main entry point; draws
114
+ the tree and returns a `CircularDendrogramResult` (figure, axes,
115
+ layout, and the `PolarTransform` used, for further annotation).
116
+ - `dendrofan.geometry.PolarTransform` — the reusable rectangular-to-polar
117
+ coordinate map, if you want to bend your own geometry onto the same
118
+ annulus (e.g. a custom decoration).
119
+ - `dendrofan.clustering.build_layout(...)` — validated wrapper around
120
+ `scipy.cluster.hierarchy.linkage` / `dendrogram`, decoupled from
121
+ plotting.
122
+ - `dendrofan.styling.resolve_leaf_colors`, `legend_handles` — color and
123
+ legend helpers.
124
+ - `dendrofan.annotations.add_scale_ring`, `highlight_sector` — optional
125
+ decorations (a distance-reference ring; shaded clade sectors).
126
+
127
+ Every public function and class has a full docstring; `help(...)` in a
128
+ Python session is the fastest way to see the complete parameter
129
+ reference.
130
+
131
+ ## Scope
132
+
133
+ dendrofan draws circular dendrograms from hierarchical clustering
134
+ (SciPy linkage matrices) — it does not parse Newick/phylogenetic tree
135
+ files or handle unequal-tip-depth phylogenies the way `ape` itself
136
+ does. If you need that, `ape` (R) or `ete3`/`Bio.Phylo` (Python) are a
137
+ better fit; dendrofan specifically fills the "I have a SciPy
138
+ dendrogram and want it circular" gap.
139
+
140
+ ## Testing
141
+
142
+ ```bash
143
+ pytest
144
+ ```
145
+
146
+ The test suite specifically exercises the edge cases listed above
147
+ (degenerate trees, mismatched labels, invalid linkage matrices, partial
148
+ spans, single/two-leaf trees).
149
+
150
+ ## Citation
151
+
152
+ If you use dendrofan in a manuscript, please cite the software itself
153
+ via its Zenodo DOI (see [`CITATION.cff`](CITATION.cff), or use GitHub's
154
+ "Cite this repository" button):
155
+
156
+ > Magallanes Quintanar, R. (2026). dendrofan v0.1.0 [Software]. Zenodo.
157
+ > https://doi.org/10.5281/zenodo.21251841
158
+
159
+ Please also cite the underlying methods it wraps:
160
+
161
+ - Virtanen, P. et al. (2020). SciPy 1.0: fundamental algorithms for
162
+ scientific computing in Python. *Nature Methods*, 17, 261-272.
163
+ *(hierarchical clustering / linkage, which dendrofan builds on)*
164
+ - Paradis, E. & Schliep, K. (2019). ape 5.0: an environment for modern
165
+ phylogenetics and evolutionary analyses in R. *Bioinformatics*,
166
+ 35(3), 526-528. *(for the fan-plot visual convention dendrofan follows)*
167
+
168
+ ## License
169
+
170
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,141 @@
1
+ # dendrofan
2
+
3
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21251841.svg)](https://doi.org/10.5281/zenodo.21251841)
4
+
5
+ Circular ("fan") dendrograms for Python, in the visual style of R's
6
+ [`ape`](https://cran.r-project.org/package=ape) package
7
+ (`plot.phylo(type = "fan")`, Paradis & Schliep, 2019) — computed and
8
+ rendered entirely with NumPy, SciPy, and Matplotlib. No R dependency,
9
+ no reimplementation of clustering.
10
+
11
+ ## Why this exists
12
+
13
+ `scipy.cluster.hierarchy` has no circular/fan layout. The usual
14
+ workaround is a one-off script: call
15
+ `scipy.cluster.hierarchy.dendrogram(..., no_plot=True)`, take its
16
+ rectangular `icoord`/`dcoord` output, and manually re-project it into
17
+ polar coordinates with Matplotlib. That works for one figure, but it
18
+ tends to:
19
+
20
+ - assume a full 360° circle with no gap for labels at the seam,
21
+ - divide by `dcoord.max()` without checking for a degenerate,
22
+ all-zero-height tree,
23
+ - hardcode SciPy's 10-units-per-leaf spacing convention,
24
+ - have no story for trees with 1 or 2 leaves,
25
+ - mix clustering, geometry, and plotting into a single script, so
26
+ none of it is reusable for the next dataset.
27
+
28
+ dendrofan factors the actually-reusable idea — the rectangular-to-polar
29
+ coordinate transform — into a small, tested, documented library, and
30
+ builds a real plotting API on top of it.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install -e . # from a source checkout
36
+ pip install -e ".[dev]" # + pytest, for running the test suite
37
+ ```
38
+
39
+ ## Quick start
40
+
41
+ ```python
42
+ import numpy as np
43
+ from dendrofan import circular_dendrogram
44
+
45
+ rng = np.random.default_rng(0)
46
+ data = rng.normal(size=(20, 6))
47
+ labels = [f"sample_{i:02d}" for i in range(20)]
48
+
49
+ result = circular_dendrogram(
50
+ data,
51
+ labels=labels,
52
+ method="ward", # forwarded to scipy.cluster.hierarchy.linkage
53
+ span=350, # leave a small angular gap at the seam
54
+ inner_radius=0.15, # small central gap, ape's fan style
55
+ )
56
+ result.fig.savefig("tree.png", dpi=200, bbox_inches="tight")
57
+ ```
58
+
59
+ `circular_dendrogram` accepts the same three kinds of input as SciPy's
60
+ own `dendrogram`: raw observations (`data=...`), a precomputed linkage
61
+ matrix (`Z=...`), or a precomputed condensed distance vector
62
+ (`condensed_distances=...`).
63
+
64
+ See [`examples/quickstart.py`](examples/quickstart.py) and
65
+ [`examples/reproduce_station_dendrogram.py`](examples/reproduce_station_dendrogram.py)
66
+ (a fully worked, colored-by-group example) for more.
67
+
68
+ ## What it handles that the ad hoc version didn't
69
+
70
+ | Case | Ad hoc script | dendrofan |
71
+ |---|---|---|
72
+ | Angular gap at the seam (for label room) | not supported (full circle only) | `span=350` (or any value `<= 360`) |
73
+ | All-merge-heights-equal-zero tree | `ZeroDivisionError` / silent NaNs | falls back to placing all nodes at the leaf radius |
74
+ | 1 or 2 leaves | untested, breaks silently | validated; raises `DegenerateTreeError` for < 2, works for 2 |
75
+ | Mismatched label count | silent misalignment | raises `LabelMismatchError` |
76
+ | Invalid linkage matrix / distance vector | undefined behavior | raises `InvalidLinkageError` before plotting |
77
+ | Root at centre vs. rim, radius scale | hardcoded linear, root-at-centre | `inner_radius`/`outer_radius`/`invert_radius`, plus optional `"sqrt"` area-preserving radius scale |
78
+ | Per-clade coloring | manual, one-off | `color_threshold`/`link_color_func` forwarded to SciPy, or a `label_colors` dict/callable |
79
+ | Scale reference / clade highlighting | not present | `dendrofan.annotations.add_scale_ring`, `highlight_sector` |
80
+ | Reuse across datasets | copy-paste the script | one function call |
81
+
82
+ ## API overview
83
+
84
+ - `dendrofan.circular_dendrogram(...)` — the main entry point; draws
85
+ the tree and returns a `CircularDendrogramResult` (figure, axes,
86
+ layout, and the `PolarTransform` used, for further annotation).
87
+ - `dendrofan.geometry.PolarTransform` — the reusable rectangular-to-polar
88
+ coordinate map, if you want to bend your own geometry onto the same
89
+ annulus (e.g. a custom decoration).
90
+ - `dendrofan.clustering.build_layout(...)` — validated wrapper around
91
+ `scipy.cluster.hierarchy.linkage` / `dendrogram`, decoupled from
92
+ plotting.
93
+ - `dendrofan.styling.resolve_leaf_colors`, `legend_handles` — color and
94
+ legend helpers.
95
+ - `dendrofan.annotations.add_scale_ring`, `highlight_sector` — optional
96
+ decorations (a distance-reference ring; shaded clade sectors).
97
+
98
+ Every public function and class has a full docstring; `help(...)` in a
99
+ Python session is the fastest way to see the complete parameter
100
+ reference.
101
+
102
+ ## Scope
103
+
104
+ dendrofan draws circular dendrograms from hierarchical clustering
105
+ (SciPy linkage matrices) — it does not parse Newick/phylogenetic tree
106
+ files or handle unequal-tip-depth phylogenies the way `ape` itself
107
+ does. If you need that, `ape` (R) or `ete3`/`Bio.Phylo` (Python) are a
108
+ better fit; dendrofan specifically fills the "I have a SciPy
109
+ dendrogram and want it circular" gap.
110
+
111
+ ## Testing
112
+
113
+ ```bash
114
+ pytest
115
+ ```
116
+
117
+ The test suite specifically exercises the edge cases listed above
118
+ (degenerate trees, mismatched labels, invalid linkage matrices, partial
119
+ spans, single/two-leaf trees).
120
+
121
+ ## Citation
122
+
123
+ If you use dendrofan in a manuscript, please cite the software itself
124
+ via its Zenodo DOI (see [`CITATION.cff`](CITATION.cff), or use GitHub's
125
+ "Cite this repository" button):
126
+
127
+ > Magallanes Quintanar, R. (2026). dendrofan v0.1.0 [Software]. Zenodo.
128
+ > https://doi.org/10.5281/zenodo.21251841
129
+
130
+ Please also cite the underlying methods it wraps:
131
+
132
+ - Virtanen, P. et al. (2020). SciPy 1.0: fundamental algorithms for
133
+ scientific computing in Python. *Nature Methods*, 17, 261-272.
134
+ *(hierarchical clustering / linkage, which dendrofan builds on)*
135
+ - Paradis, E. & Schliep, K. (2019). ape 5.0: an environment for modern
136
+ phylogenetics and evolutionary analyses in R. *Bioinformatics*,
137
+ 35(3), 526-528. *(for the fan-plot visual convention dendrofan follows)*
138
+
139
+ ## License
140
+
141
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "dendrofan"
7
+ version = "0.1.1"
8
+ description = "Circular (\"fan\") dendrograms for Python, in the visual style of R's ape package"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.8"
12
+ authors = [
13
+ { name = "Rafael Magallanes Quintanar" },
14
+ ]
15
+ keywords = [
16
+ "dendrogram",
17
+ "hierarchical-clustering",
18
+ "circular-plot",
19
+ "fan-plot",
20
+ "phylogenetics",
21
+ "data-visualization",
22
+ "scipy",
23
+ "matplotlib",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 4 - Beta",
27
+ "Intended Audience :: Science/Research",
28
+ "License :: OSI Approved :: MIT License",
29
+ "Programming Language :: Python :: 3",
30
+ "Programming Language :: Python :: 3 :: Only",
31
+ "Topic :: Scientific/Engineering :: Visualization",
32
+ "Topic :: Scientific/Engineering :: Bio-Informatics",
33
+ "Topic :: Scientific/Engineering :: Mathematics",
34
+ ]
35
+ dependencies = [
36
+ "numpy>=1.20",
37
+ "scipy>=1.7",
38
+ "matplotlib>=3.4",
39
+ ]
40
+
41
+ [project.optional-dependencies]
42
+ dev = ["pytest>=7", "pytest-mpl", "build"]
43
+
44
+ [project.urls]
45
+ Homepage = "https://github.com/tiquis/dendrofan"
46
+ Issues = "https://github.com/tiquis/dendrofan/issues"
47
+
48
+ [tool.setuptools.packages.find]
49
+ where = ["src"]
50
+
51
+ [tool.pytest.ini_options]
52
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,62 @@
1
+ """
2
+ dendrofan: circular ("fan") dendrograms for Python.
3
+
4
+ dendrofan draws dendrograms bent onto an annulus, in the visual style
5
+ of ``ape::plot.phylo(type = "fan")`` in R (Paradis & Schliep, 2019),
6
+ using only NumPy, SciPy, and Matplotlib -- no R dependency, no
7
+ reimplementation of clustering. It exists because
8
+ ``scipy.cluster.hierarchy`` has no circular layout mode: the usual
9
+ workaround is a one-off script that manually re-projects
10
+ ``dendrogram()``'s rectangular ``icoord``/``dcoord`` output into polar
11
+ coordinates. dendrofan generalizes that idea into a small, tested
12
+ library: a documented coordinate transform
13
+ (:class:`dendrofan.geometry.PolarTransform`) plus a plotting API that
14
+ handles the edge cases a one-off script usually skips (single-leaf and
15
+ two-leaf trees, all-zero merge heights, mismatched labels, partial fans
16
+ with an angular gap, inward vs. outward root, etc).
17
+
18
+ Quick start
19
+ -----------
20
+ >>> import numpy as np
21
+ >>> from dendrofan import circular_dendrogram
22
+ >>> rng = np.random.default_rng(0)
23
+ >>> data = rng.normal(size=(12, 5))
24
+ >>> result = circular_dendrogram(data, labels=[f"s{i}" for i in range(12)])
25
+ >>> result.fig.savefig("tree.png", dpi=200)
26
+
27
+ See :func:`circular_dendrogram` for the full parameter reference, and
28
+ the ``examples/`` directory in the source distribution for worked
29
+ examples, including one that reproduces a colored-by-group station
30
+ dendrogram end to end.
31
+ """
32
+ from ._version import __version__
33
+ from .annotations import add_scale_ring, highlight_sector
34
+ from .clustering import DendrogramLayout, build_layout
35
+ from .exceptions import (
36
+ DegenerateTreeError,
37
+ DendrofanError,
38
+ InvalidLinkageError,
39
+ LabelMismatchError,
40
+ )
41
+ from .geometry import PolarTransform, arc_points, radial_label_alignment
42
+ from .plotting import CircularDendrogramResult, circular_dendrogram
43
+ from .styling import legend_handles, resolve_leaf_colors
44
+
45
+ __all__ = [
46
+ "__version__",
47
+ "circular_dendrogram",
48
+ "CircularDendrogramResult",
49
+ "build_layout",
50
+ "DendrogramLayout",
51
+ "PolarTransform",
52
+ "arc_points",
53
+ "radial_label_alignment",
54
+ "add_scale_ring",
55
+ "highlight_sector",
56
+ "resolve_leaf_colors",
57
+ "legend_handles",
58
+ "DendrofanError",
59
+ "InvalidLinkageError",
60
+ "LabelMismatchError",
61
+ "DegenerateTreeError",
62
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.1"
@@ -0,0 +1,109 @@
1
+ """Optional decorations for a circular dendrogram: scale rings and sector highlights.
2
+
3
+ These operate on the ``ax``/``transform`` pair returned by
4
+ :func:`dendrofan.plotting.circular_dendrogram`, so they can be added or
5
+ omitted independently of the tree itself.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from typing import Optional, Sequence
10
+
11
+ import numpy as np
12
+ from matplotlib.axes import Axes
13
+
14
+ from .geometry import PolarTransform
15
+
16
+
17
+ def add_scale_ring(
18
+ ax: Axes,
19
+ transform: PolarTransform,
20
+ heights: Sequence[float],
21
+ *,
22
+ color: str = "gray",
23
+ linewidth: float = 0.6,
24
+ linestyle: str = "--",
25
+ label_heights: bool = True,
26
+ label_fontsize: float = 8,
27
+ label_angle: float = 90.0,
28
+ ) -> None:
29
+ """Draw concentric guide circles at the given linkage heights.
30
+
31
+ The circular analogue of the distance axis ``ape::axisPhylo()``
32
+ draws on a rectangular/fan tree: a light dashed ring at each height
33
+ in ``heights``, optionally labeled with the height value, so
34
+ readers can gauge merge distances even though a circular layout
35
+ has no literal axis line to read off.
36
+
37
+ Parameters
38
+ ----------
39
+ heights : sequence of float
40
+ Linkage heights (in the original units of the tree, i.e. of
41
+ ``dcoord`` / the linkage matrix) at which to draw a ring.
42
+ Heights outside ``[0, transform.y_max]`` are skipped.
43
+ label_angle : float, default 90
44
+ Angle, in degrees, at which each ring's height label is placed
45
+ (90 = top of the circle, matching ``start_angle``'s default).
46
+ """
47
+ theta = np.radians(label_angle)
48
+ for h in heights:
49
+ if h < 0 or h > transform.y_max:
50
+ continue
51
+ r = float(transform.radius(h))
52
+ circle = np.linspace(0, 2 * np.pi, 200)
53
+ ax.plot(
54
+ r * np.cos(circle),
55
+ r * np.sin(circle),
56
+ color=color,
57
+ linewidth=linewidth,
58
+ linestyle=linestyle,
59
+ zorder=0,
60
+ )
61
+ if label_heights:
62
+ ax.text(
63
+ r * np.cos(theta),
64
+ r * np.sin(theta),
65
+ f"{h:g}",
66
+ fontsize=label_fontsize,
67
+ color=color,
68
+ ha="center",
69
+ va="bottom",
70
+ )
71
+
72
+
73
+ def highlight_sector(
74
+ ax: Axes,
75
+ transform: PolarTransform,
76
+ theta_start: float,
77
+ theta_end: float,
78
+ *,
79
+ r_inner: Optional[float] = None,
80
+ r_outer: Optional[float] = None,
81
+ color: str = "gray",
82
+ alpha: float = 0.15,
83
+ n_points: int = 100,
84
+ zorder: float = -1,
85
+ ) -> None:
86
+ """Shade a wedge of the plot behind a clade, the way ``ape``'s clade
87
+ highlighting (e.g. via ``ape::ring`` or manually drawn background
88
+ wedges) marks a group of related leaves.
89
+
90
+ Parameters
91
+ ----------
92
+ theta_start, theta_end : float
93
+ Angular bounds of the wedge, in radians (e.g. from
94
+ ``CircularDendrogramResult.leaf_theta_by_index``).
95
+ r_inner, r_outer : float, optional
96
+ Radial bounds of the wedge. Default to
97
+ ``transform.inner_radius`` and ``transform.outer_radius``
98
+ respectively, i.e. the whole tree depth.
99
+ """
100
+ r_inner = transform.inner_radius if r_inner is None else r_inner
101
+ r_outer = transform.outer_radius if r_outer is None else r_outer
102
+
103
+ thetas = np.linspace(theta_start, theta_end, n_points)
104
+ outer_x, outer_y = r_outer * np.cos(thetas), r_outer * np.sin(thetas)
105
+ inner_x, inner_y = r_inner * np.cos(thetas[::-1]), r_inner * np.sin(thetas[::-1])
106
+
107
+ xs = np.concatenate([outer_x, inner_x])
108
+ ys = np.concatenate([outer_y, inner_y])
109
+ ax.fill(xs, ys, color=color, alpha=alpha, linewidth=0, zorder=zorder)