Source code for glyphx.kde
"""
GlyphX KDESeries — standalone kernel density estimate curve.
Uses the pure-NumPy Gaussian KDE from violin_plot.py — no scipy required.
from glyphx import Figure
from glyphx.kde import KDESeries
fig = Figure()
fig.add(KDESeries(data, label="Control", color="#3b82f6"))
fig.add(KDESeries(data2, label="Treatment", color="#ef4444", filled=True))
fig.show()
"""
from __future__ import annotations
import numpy as np
from .series import BaseSeries
from .utils import svg_escape
from .violin_plot import _numpy_kde
[docs]
class KDESeries(BaseSeries):
"""
Smooth kernel density estimate curve (no scipy required).
Args:
data: 1-D array of raw observations.
n_points: Number of evaluation points along the curve (default 200).
filled: If ``True``, shade the area under the curve.
alpha: Fill opacity when ``filled=True`` (default 0.20).
color: Line (and fill) color.
width: Line stroke width in pixels.
label: Legend label.
bw_method: Bandwidth: ``"silverman"`` (default) or a positive float
multiplier applied to the Silverman estimate.
"""
def __init__(
self,
data,
n_points: int = 200,
filled: bool = False,
alpha: float = 0.20,
color: str = "#1f77b4",
width: int = 2,
label: str | None = None,
bw_method: str | float = "silverman",
) -> None:
arr = np.asarray(data, dtype=float)
arr = arr[np.isfinite(arr)]
h = None if bw_method == "silverman" else float(bw_method)
kde = _numpy_kde(arr, bandwidth=h)
x_range = np.linspace(arr.min(), arr.max(), n_points)
y_density = kde(x_range)
self.kde_x = x_range.tolist()
self.kde_y = y_density.tolist()
self.filled = filled
self.alpha = float(alpha)
self.width = int(width)
self.css_class = f"series-{id(self) % 100000}"
super().__init__(
x = self.kde_x,
y = self.kde_y,
color = color,
label = label,
)
[docs]
def to_svg(self, ax: object, use_y2: bool = False) -> str:
scale_y = ax.scale_y2 if use_y2 else ax.scale_y # type: ignore[union-attr]
x_vals = getattr(self, "_numeric_x", self.kde_x)
pts = " ".join(
f"{ax.scale_x(x):.2f},{scale_y(y):.2f}" # type: ignore[union-attr]
for x, y in zip(x_vals, self.kde_y)
)
elements = []
if self.filled:
# Close the polygon to x-axis (y=0)
y0 = scale_y(0) # type: ignore[union-attr]
x_left = ax.scale_x(x_vals[0]) # type: ignore[union-attr]
x_right = ax.scale_x(x_vals[-1]) # type: ignore[union-attr]
polygon_pts = f"{x_left},{y0} " + pts + f" {x_right},{y0}"
elements.append(
f'<polygon class="{self.css_class}" '
f'points="{polygon_pts}" '
f'fill="{self.color}" fill-opacity="{self.alpha}" stroke="none"/>'
)
elements.append(
f'<polyline class="{self.css_class}" fill="none" '
f'stroke="{self.color}" stroke-width="{self.width}" '
f'points="{pts}"/>'
)
return "\n".join(elements)