Coverage for src/pyhrp/plot.py: 100%
16 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-17 14:01 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-17 14:01 +0000
1"""Plotly visualization for hierarchical clustering dendrograms.
3Kept separate from the allocation core (``hrp.py``) so that the optional
4plotting dependency (plotly) and the visualization logic do not couple the
5algorithm modules. ``Dendrogram.plot`` delegates here.
6"""
8from __future__ import annotations
10from typing import TYPE_CHECKING
12import plotly.graph_objects as go
13import scipy.cluster.hierarchy as sch
15if TYPE_CHECKING:
16 from .dendrogram import Dendrogram
18__all__ = ["plot_dendrogram"]
21def plot_dendrogram(dendrogram: Dendrogram, **kwargs: object) -> go.Figure:
22 """Build and return a plotly dendrogram figure for a Dendrogram.
24 Args:
25 dendrogram (Dendrogram): Clustering result carrying the linkage matrix
26 and asset labels to render.
27 **kwargs (object): Extra keyword arguments forwarded to
28 ``scipy.cluster.hierarchy.dendrogram`` (e.g. ``color_threshold``).
30 Returns:
31 go.Figure: A plotly figure drawing the dendrogram as line traces.
33 Raises:
34 ValueError: If the dendrogram has no linkage matrix to plot.
36 Examples:
37 >>> import polars as pl
38 >>> from pyhrp.hrp import build_tree
39 >>> from pyhrp.plot import plot_dendrogram
40 >>> cor = pl.DataFrame({"A": [1.0, 0.5], "B": [0.5, 1.0]})
41 >>> fig = plot_dendrogram(build_tree(cor, method="ward"))
42 >>> type(fig).__name__
43 'Figure'
44 """
45 if dendrogram.linkage is None:
46 msg = "Dendrogram has no linkage matrix to plot."
47 raise ValueError(msg)
48 ddata = sch.dendrogram(dendrogram.linkage, labels=dendrogram.assets, no_plot=True, **kwargs)
49 fig = go.Figure()
50 for xs, ys in zip(ddata["icoord"], ddata["dcoord"], strict=False):
51 fig.add_trace(go.Scatter(x=xs, y=ys, mode="lines", line={"color": "steelblue"}, showlegend=False))
52 n = len(dendrogram.assets)
53 fig.update_layout(
54 xaxis={
55 "tickmode": "array",
56 "tickvals": [5 + 10 * i for i in range(n)],
57 "ticktext": ddata["ivl"],
58 "tickangle": -90,
59 },
60 )
61 return fig