Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 30 additions & 20 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from matplotlib.axes._secondary_axes import SecondaryAxis
from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer


_log = logging.getLogger(__name__)


Expand Down Expand Up @@ -6870,28 +6871,37 @@ def hist(self, x, bins=None, range=None, density=False, weights=None,
xvals.append(x.copy())
yvals.append(y.copy())

# stepfill is closed, step is not
split = -1 if fill else 2 * len(bins)
# add patches in reverse order so that when stacking,
# items lower in the stack are plotted on top of
# items higher in the stack
for x, y, c in reversed(list(zip(xvals, yvals, color))):
patches.append(self.fill(
x[:split], y[:split],
closed=True if fill else None,
facecolor=c,
fill_kwarg = kwargs.get('fill', None)
if fill_kwarg is None:
fill_requested = False
elif (np.iterable(fill_kwarg)
and not isinstance(fill_kwarg, (str, bytes))):
fill_requested = any(fill_kwarg)
else:
fill_requested = bool(fill_kwarg)
polygon_lists = []
for x_vals, y_vals, c in reversed(list(zip(xvals, yvals, color))):
vertices = np.column_stack([x_vals[:split], y_vals[:split]])
facecolor = c if fill or fill_requested else 'none'
poly = mpatches.Polygon(
vertices,
closed=fill,
fill=fill,
edgecolor=None if fill else c,
fill=fill if fill else None,
zorder=None if fill else mlines.Line2D.zorder))
for patch_list in patches:
for patch in patch_list:
if orientation == 'vertical':
patch.sticky_edges.y.append(0)
elif orientation == 'horizontal':
patch.sticky_edges.x.append(0)

# we return patches, so put it back in the expected order
patches.reverse()
facecolor=facecolor,
zorder=None if fill else mlines.Line2D.zorder,
)
poly.get_path().should_simplify = False
self.add_patch(poly)
if orientation == 'vertical':
poly.sticky_edges.y.append(0)
else: # orientation == 'horizontal'
poly.sticky_edges.x.append(0)
polygon_lists.append([poly])

patches = list(reversed(polygon_lists))
self._request_autoscale_view()

# If None, make all labels None (via zip_longest below); otherwise,
# cast each element to str, but keep a single str as it.
Expand Down
60 changes: 60 additions & 0 deletions lib/matplotlib/tests/test_axes/test_hist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from __future__ import annotations

import numpy as np
import pytest

import matplotlib.pyplot as plt


def _generate_density_samples(
*,
seed: int = 20250101,
size: int = 100_000,
scale: float = 1.2,
) -> np.ndarray:
rng = np.random.default_rng(seed)
return rng.standard_normal(size) * scale


def _hist_peak_and_ylim(
histtype: str, values: np.ndarray,
) -> tuple[float, float]:
fig, ax = plt.subplots()
try:
counts, _, _ = ax.hist(
values,
bins=100,
density=True,
histtype=histtype,
)
peak = float(counts.max()) if counts.size else 0.0
upper = float(ax.get_ylim()[1])
finally:
plt.close(fig)
return peak, upper


def test_hist_step_density_autoscale_includes_peak() -> None:
values = _generate_density_samples()
peak, upper = _hist_peak_and_ylim("step", values)
assert upper >= peak


def test_hist_step_density_scale_invariance() -> None:
values = _generate_density_samples()
peak_base, upper_base = _hist_peak_and_ylim("step", values)
scaled_values = values * 3.0
peak_scaled, upper_scaled = _hist_peak_and_ylim("step", scaled_values)

assert upper_base >= peak_base
assert upper_scaled >= peak_scaled

ratio_base = upper_base / peak_base
ratio_scaled = upper_scaled / peak_scaled
assert ratio_scaled == pytest.approx(ratio_base, rel=0.05)


def test_hist_stepfilled_density_autoscale_includes_peak() -> None:
values = _generate_density_samples()
peak, upper = _hist_peak_and_ylim("stepfilled", values)
assert upper >= peak