diff --git a/python/xy/pyplot/_axes.py b/python/xy/pyplot/_axes.py index 6710fd18..2d4b45f0 100644 --- a/python/xy/pyplot/_axes.py +++ b/python/xy/pyplot/_axes.py @@ -186,7 +186,11 @@ def convert(values: Any) -> Any: indexes = { "segments": (0, 2) if axis == "x" else (1, 3), "triangle_mesh": (0, 2, 4) if axis == "x" else (1, 3, 5), + "area": (0,) if axis == "x" else (1,), "step": (0,) if axis == "x" else (1,), + # Compact stairs store (values, edges), the reverse of ordinary + # Cartesian (x, y) argument order. + "stairs": (1,) if axis == "x" else (0,), "stem": (0,) if axis == "x" else (1,), "errorbar": (0,) if axis == "x" else (1,), "hexbin": (0,) if axis == "x" else (1,), @@ -195,6 +199,8 @@ def convert(values: Any) -> Any: for index in indexes: args[index] = convert(args[index]) entry["args"] = tuple(args) + if factory == "area" and axis == "y" and "base" in entry.get("kwargs", {}): + entry["kwargs"]["base"] = convert(entry["kwargs"]["base"]) def _nonlinear_ticks(domain: tuple[float, float], spec: dict[str, Any]) -> np.ndarray: @@ -2697,9 +2703,17 @@ def _iter_entry_arrays(self, axis: str) -> Iterator[tuple[np.ndarray, bool]]: indexes = { "segments": (0, 2) if axis == "x" else (1, 3), "triangle_mesh": (0, 2, 4) if axis == "x" else (1, 3, 5), + "area": (0,) if axis == "x" else (1,), + # xy.stairs(values, edges) is compact and deliberately + # reverses the ordinary x/y argument order. + "stairs": (1,) if axis == "x" else (0,), }.get(factory, ()) for index in indexes: yield np.asarray(entry["args"][index], dtype=np.float64).reshape(-1), True + if factory == "area" and axis == "y": + base = entry.get("kwargs", {}).get("base") + if base is not None: + yield np.asarray(base, dtype=np.float64).reshape(-1), True if factory == "contour": z = np.asarray(entry["args"][0]) coordinates = entry.get("kwargs", {}).get(key) diff --git a/tests/pyplot/test_gallery_hist_errorbar_compat.py b/tests/pyplot/test_gallery_hist_errorbar_compat.py index 87579ec1..86384705 100644 --- a/tests/pyplot/test_gallery_hist_errorbar_compat.py +++ b/tests/pyplot/test_gallery_hist_errorbar_compat.py @@ -62,6 +62,51 @@ def test_hist_dataset_style_lengths_must_match_dataset_count() -> None: ax.hist([[0, 1], [2, 3]], bins=2, linewidth=[1, 2, 3]) +@pytest.mark.parametrize("histtype", ["step", "stepfilled"]) +def test_hist_step_geometry_contributes_to_autoscale(histtype: str) -> None: + _fig, ax = plt.subplots() + + counts, edges, _container = ax.hist( + [-3.0, -1.0, -0.5, 0.0, 0.5, 1.0, 3.0], + bins=np.arange(-4.0, 4.1, 0.5), + histtype=histtype, + weights=np.full(7, 1 / 7), + ) + + assert not ax._axis_is_dataless("x") + assert not ax._axis_is_dataless("y") + assert ax._entry_extent("x") == pytest.approx((edges[0], edges[-1])) + assert ax._entry_extent("y") == pytest.approx((0.0, counts.max())) + + core = ax._build_chart(350, 300).figure() + assert core.x_range() == pytest.approx((-4.24, 4.24)) + assert core.y_range()[1] > counts.max() + + +def test_hist_density_and_probability_weights_match_numpy() -> None: + values = np.array([-2.2, -1.8, -0.4, -0.1, 0.2, 0.7, 1.1, 2.4]) + edges = np.array([-3.0, -1.0, 0.0, 0.5, 1.5, 3.0]) + _fig, (density_ax, weights_ax) = plt.subplots(1, 2) + + density, returned_edges, _ = density_ax.hist(values, bins=edges, density=True, histtype="step") + weighted, _, _ = weights_ax.hist( + values, + bins=edges, + weights=np.full(len(values), 1 / len(values)), + histtype="step", + ) + + expected_density, expected_edges = np.histogram(values, bins=edges, density=True) + expected_weighted, _ = np.histogram( + values, bins=edges, weights=np.full(len(values), 1 / len(values)) + ) + np.testing.assert_allclose(density, expected_density) + np.testing.assert_array_equal(returned_edges, expected_edges) + np.testing.assert_allclose(weighted, expected_weighted) + assert np.sum(density * np.diff(edges)) == pytest.approx(1.0) + assert weighted.sum() == pytest.approx(1.0) + + def test_errorbar_forwards_marker_size_and_linestyle_to_data_line_only() -> None: _fig, ax = plt.subplots() container = ax.errorbar(