From 1884881b1cf27a421cdc2dbf6cbe9c207daf9856 Mon Sep 17 00:00:00 2001 From: gepcel Date: Fri, 14 Aug 2026 15:41:01 +0800 Subject: [PATCH 1/2] Add kde support for hist --- ultraplot/axes/plot.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ultraplot/axes/plot.py b/ultraplot/axes/plot.py index 6bd62acaf..2a28b6a29 100644 --- a/ultraplot/axes/plot.py +++ b/ultraplot/axes/plot.py @@ -7114,6 +7114,8 @@ def _apply_hist( filled=None, histtype=None, orientation="vertical", + kde=False, + kde_kw=None, **kwargs, ): """ @@ -7157,6 +7159,24 @@ def _apply_hist( if type(sub) is list: res[i] = cbook.silent_list("Polygon", sub) self._update_guide(res, **guide_kw) + # add kde line + if not kde: + return obj + from scipy.stats import gaussian_kde + edges = obj[1] + kde_kw = dict(kde_kw or {}) + density = kw.get('density', False) + data2d = xs if xs.ndim > 1 else xs[:, None] # (M, N) data + stepsize = kde_kw.pop('stepsize', 300) + for i in range(data2d.shape[1]): + _x = data2d[:, i] + xa = np.linspace(_x.min(), _x.max(), stepsize) + ya = gaussian_kde(_x)(xa) + if not density: + idx = np.clip(np.digitize(xa, edges)-1, 0, len(edges)-2) + ya = ya * len(_x) * np.diff(edges)[idx] + x_line, y_line = (xa, ya) if orientation=="vertical" else (ya, xa) + self._call_native("plot", x_line, y_line, **kde_kw) return obj @inputs._preprocess_or_redirect("x", "bins", keywords="weights") From d94ea35ae663afa45d2db08624c8029a3a39bbe0 Mon Sep 17 00:00:00 2001 From: gepcel Date: Fri, 14 Aug 2026 18:00:27 +0800 Subject: [PATCH 2/2] add some tests --- ultraplot/tests/test_1dplots.py | 36 +++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/ultraplot/tests/test_1dplots.py b/ultraplot/tests/test_1dplots.py index c04486a12..aa49d603c 100644 --- a/ultraplot/tests/test_1dplots.py +++ b/ultraplot/tests/test_1dplots.py @@ -243,6 +243,42 @@ def test_histogram_types(rng): ax.hist(data, ec="k", **kw) return fig +def test_hist_kde_lines(rng): + """ + Test kde for hist. + """ + data = rng.normal(size=200) + # No kde if no kde is not given + fig, ax = uplt.subplot() + ax.hist(data, bins=20) + assert len(ax.lines) == 0 + # No kde if kde=False + ax.hist(data, bins=20, kde=False) + assert len(ax.lines) == 0 + # One kde line with + ax.hist(data, bins=20, kde=True) + assert len(ax.lines) == 1 + # test step size + line = ax.lines[-1] + # default stepsize is 300 + assert line.get_xdata().size == 300 + assert line.get_ydata().size == 300 + assert line.get_xdata()[0] == pytest.approx(data.min()) + assert line.get_xdata()[-1] == pytest.approx(data.max()) + # use kde_kw to set stepsize=150 + ax.hist(data, bins=20, kde=True, density=True, + kde_kw={'stepsize': 150}) + density_line = ax.lines[-1] + assert density_line.get_xdata().size == 150 + assert density_line.get_ydata().size == 150 + # test density, default is False, but to to test accurate? + assert line.get_ydata().max() > 1.0 + assert density_line.get_ydata().max() <= 1.0 + # test area==1 + area = np.trapezoid(density_line.get_ydata(), density_line.get_xdata()) + assert area == pytest.approx(1.0, rel=1e-2) + uplt.close(fig) + @pytest.mark.mpl_image_compare def test_invalid_plot(rng):