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
17 changes: 17 additions & 0 deletions js/src/10_colormaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,25 @@
// interpolates a 256-texel LUT texture once per colormap.
// ---------------------------------------------------------------------------

function flagStops() {
// Matplotlib's flag colormap is an intentionally high-frequency
// red → white → blue → black cycle. Sample its three canonical channel
// functions at the client LUT's native 256 positions so the oscillations
// are not flattened by the compact-stop interpolation used by other maps.
const clip = (value) => Math.max(0, Math.min(1, value));
return Array.from({ length: 256 }, (_, index) => {
const x = index / 255;
return [
Math.trunc(255 * clip(0.75 * Math.sin((x * 31.5 + 0.25) * Math.PI) + 0.5)),
Math.trunc(255 * clip(Math.sin(x * 31.5 * Math.PI))),
Math.trunc(255 * clip(0.75 * Math.sin((x * 31.5 - 0.25) * Math.PI) + 0.5)),
];
});
}

const COLORMAP_STOPS = {
binary: [[255, 255, 255], [0, 0, 0]],
flag: flagStops(),
reds: [[255, 245, 240], [254, 229, 216], [253, 202, 181], [252, 171, 143], [252, 138, 106], [251, 105, 74], [241, 68, 50], [217, 37, 35], [188, 20, 26], [152, 12, 19], [103, 0, 13]],
bone: [[0, 0, 0], [22, 22, 30], [45, 45, 62], [66, 66, 93], [89, 92, 121], [112, 123, 144], [134, 154, 166], [157, 185, 188], [185, 210, 210], [221, 233, 233], [255, 255, 255]],
autumn: [[255, 0, 0], [255, 25, 0], [255, 51, 0], [255, 76, 0], [255, 102, 0], [255, 128, 0], [255, 153, 0], [255, 179, 0], [255, 204, 0], [255, 230, 0], [255, 255, 0]],
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 54 additions & 0 deletions pr-assets/matplotlib-gallery-acceptance/provenance.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"matplotlib_version": "3.11.0",
"base_code_commit": "10b22b97173e6e58c6aa3fcae58e7e8776903dcf",
"head_code_commit": "97bfeb6b5eb34acea66cc054c9dab6e4a2d31216",
"full_gallery_verified_code_commit": "97bfeb6b5eb34acea66cc054c9dab6e4a2d31216",
"full_gallery_execution": {
"in_scope_examples": 88,
"paired_examples": 81,
"failed_examples": 7,
"paired_figures": 143
},
"pdsh_source_commit": "9b080851617c7fe3547a77bbb0726f57c0ac8158",
"pdsh_verified_code_commit": "97bfeb6b5eb34acea66cc054c9dab6e4a2d31216",
"pdsh_execution": {
"included_notebooks": 13,
"excluded_notebooks": [
"pdsh_04_12_three_dimensional_plotting.ipynb"
],
"applicable_cells": 154,
"passed_cells": 153,
"failed_cells": 1,
"figure_captures": 289,
"capture_errors": 0
},
"full_frame": true,
"source_tree_note": "The exact full-gallery and PDSH reruns executed at 97bfeb6 after Stack B was rebased onto Stack A c196c12. The results remained 81/88 paired gallery examples with 143 paired figures and 153/154 applicable PDSH cells with 289 captures and no capture errors. The comparison sheets were refreshed from those exact render-code boundaries.",
"comparisons": [
{
"source": "statistics/violinplot.py",
"figure": 0,
"file": "violinplot.png",
"rendered_code_commit": "97bfeb6b5eb34acea66cc054c9dab6e4a2d31216",
"dimensions": [
3024,
540
],
"sha256": "26aef7df7896a403311a4af04a63f8ebc2f2fc48e7ce1212979b8a1e800e88ff"
},
{
"source": "examples/pdsh/pdsh_04_09_text_and_annotation.ipynb",
"physical_cell": 23,
"selected_xy_ordinal": 8,
"source_id": "59bbdb311c014d738909a11f9e486628",
"figure": 4,
"file": "pdsh-text-annotation.png",
"rendered_code_commit": "97bfeb6b5eb34acea66cc054c9dab6e4a2d31216",
"dimensions": [
2412,
530
],
"sha256": "328b8e6587dbbde7d69bde294083e3f89a5a7659a4b86000d1277c68deb61056"
}
]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions python/xy/_svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,26 @@ def _stroke_opacity(style: dict[str, Any], default: float = 1.0) -> float:
return float(style.get("opacity", default)) * float(style.get("stroke_opacity", 1.0))


def _flag_stops() -> list[tuple[int, int, int]]:
"""Matplotlib's high-frequency ``flag`` map at the native 256 LUT positions."""
x = np.linspace(0.0, 1.0, 256)
channels = np.column_stack(
(
0.75 * np.sin((x * 31.5 + 0.25) * np.pi) + 0.5,
np.sin(x * 31.5 * np.pi),
0.75 * np.sin((x * 31.5 - 0.25) * np.pi) + 0.5,
)
)
# Match Matplotlib's ``bytes=True`` conversion, which truncates rather than
# rounds each clipped channel after scaling it to the uint8 range.
rgb = (np.clip(channels, 0.0, 1.0) * 255.0).astype(np.uint8)
return [tuple(int(channel) for channel in row) for row in rgb]


# Mirrors js/src/10_colormaps.ts COLORMAP_STOPS (§36) — test-guarded.
COLORMAP_STOPS: dict[str, list[tuple[int, int, int]]] = {
"binary": [(255, 255, 255), (0, 0, 0)],
"flag": _flag_stops(),
"reds": [
(255, 245, 240),
(254, 229, 216),
Expand Down
1 change: 1 addition & 0 deletions python/xy/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"rdbu",
"jet",
"binary",
"flag",
"reds",
"bone",
"winter",
Expand Down
1 change: 1 addition & 0 deletions python/xy/pyplot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2896,6 +2896,7 @@ def __iter__(self) -> Iterator[str]:
"RdGy",
"bwr",
"jet",
"flag",
"Blues",
"RdYlGn",
"rainbow",
Expand Down
10 changes: 10 additions & 0 deletions python/xy/pyplot/_artists.py
Original file line number Diff line number Diff line change
Expand Up @@ -1203,6 +1203,16 @@ def set_clim(self, vmin: Any = None, vmax: Any = None) -> None:
class Wedge(PolyCollection):
"""Pie wedge backed by a grouped subset of one native sector mesh."""

@property
def theta1(self) -> float:
"""Starting angle in degrees, matching Matplotlib's public geometry."""
return float(self._entry["pie_theta1"])

@property
def theta2(self) -> float:
"""Ending angle in degrees, matching Matplotlib's public geometry."""
return float(self._entry["pie_theta2"])


class PieContainer:
"""Matplotlib 3.11-compatible pie result with legacy tuple unpacking."""
Expand Down
32 changes: 32 additions & 0 deletions python/xy/pyplot/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1784,6 +1784,33 @@ def _bar_like(
orientation: str,
kwargs: dict[str, Any],
) -> BarContainer:
def materialize_iterable(value: Any) -> Any:
"""Turn one-shot and view iterables into real bar columns.

``np.asarray(dict.values())`` is a scalar object array, which made
one apparent bar and left the raw view to fail later during
autoscale. Array-like containers such as Series already expose a
meaningful ndarray and should stay untouched.
"""
# Keep the common bar path allocation-neutral. In particular,
# probing a list of category labels with ``np.asarray`` here and
# again below doubled the string-array conversion cost.
if value is None or isinstance(value, (str, bytes, list, tuple, range, np.ndarray)):
return value
if np.isscalar(value):
return value
array = np.asarray(value)
if array.ndim != 0:
return value
try:
return list(value)
except TypeError:
return value

cats = materialize_iterable(cats)
vals = materialize_iterable(vals)
thickness = materialize_iterable(thickness)
base = materialize_iterable(base)
cat_array = np.asarray(cats)
if cat_array.dtype.kind == "U" and cat_array.dtype.isnative:
# _plain_text only rewrites labels containing TeX markers; a
Expand Down Expand Up @@ -5971,6 +5998,11 @@ def axis_values(values: Any, axis: str) -> np.ndarray:
return np.asarray(unit_converted_values(values), dtype=np.float64).reshape(-1)
except (TypeError, ValueError):
array = np.asanyarray(values).reshape(-1)
if self._axis_holds_datetimes(axis):
converted = [self._data_coordinate(value, axis) for value in array]
if not all(value is not None for value in converted):
raise
return np.asarray(converted, dtype=np.float64)
if not all(isinstance(value, str) for value in array):
raise
mapping = category_maps.setdefault(axis, {})
Expand Down
1 change: 1 addition & 0 deletions python/xy/pyplot/_colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ def scalar_float(value: Any) -> float:
"rdgy": "rdgy",
"jet": "jet",
"binary": "binary",
"flag": "flag",
"reds": "reds",
"bone": "bone",
"winter": "winter",
Expand Down
26 changes: 18 additions & 8 deletions python/xy/pyplot/_plot_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2492,9 +2492,14 @@ def silverman_factor(self) -> float:
}
vpstats.append(item)
if quantiles is not None:
if len(quantiles) != len(groups):
quantile_groups = list(quantiles)
if quantile_groups and all(np.isscalar(value) for value in quantile_groups):
quantile_groups = [quantile_groups]
elif not quantile_groups:
quantile_groups = [[] for _ in groups]
if len(quantile_groups) != len(groups):
raise ValueError("quantiles must contain one sequence per violin")
for item, group, requested in zip(vpstats, groups, quantiles, strict=True):
for item, group, requested in zip(vpstats, groups, quantile_groups, strict=True):
item["quantiles"] = np.quantile(group[np.isfinite(group)], requested)
return self.violin(
vpstats,
Expand Down Expand Up @@ -2820,11 +2825,13 @@ def _contour(self, filled: bool, args: tuple[Any, ...], kwargs: dict[str, Any])
corner_mask = rcParams["contour.corner_mask"]
if not isinstance(corner_mask, (bool, np.bool_)):
raise TypeError("corner_mask must be a boolean")
extend = kwargs.pop("extend", None)
if extend is None:
extend = "neither"
if extend not in ("neither", "min", "max", "both"):
raise ValueError("extend must be 'neither', 'min', 'max', or 'both'")
public_extend = kwargs.pop("extend", None)
if public_extend is None:
public_extend = "neither"
# Matplotlib stores unrecognized public values but treats them as an
# unextended contour. Keep that observable value on the ContourSet
# while passing the renderer its normalized four-value contract.
extend = public_extend if public_extend in ("neither", "min", "max", "both") else "neither"
hatches = kwargs.pop("hatches", None)
locator = kwargs.pop("locator", None)
za = np.asarray(z, dtype=np.float64)
Expand Down Expand Up @@ -2979,7 +2986,7 @@ def _contour(self, filled: bool, args: tuple[Any, ...], kwargs: dict[str, Any])
"corner_mask": bool(corner_mask),
"domain": (float(public_levels[0]), float(public_levels[-1])),
"hatches": list(hatches) if hatches is not None else None,
"extend": extend,
"extend": public_extend,
"levels": public_levels,
},
)
Expand Down Expand Up @@ -4273,6 +4280,9 @@ def pie(
entry["pie_mid"] = float(mids[index])
entry["pie_radius"] = float(radius)
entry["pie_explode"] = float(offsets[index])
theta_start, theta_end = np.rad2deg(boundaries[index : index + 2])
entry["pie_theta1"] = float(min(theta_start, theta_end))
entry["pie_theta2"] = float(max(theta_start, theta_end))
wedges.append(Wedge(self, entry))

angle = np.deg2rad(float(startangle))
Expand Down
11 changes: 7 additions & 4 deletions tests/pyplot/test_best_legend_placement.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,13 +362,16 @@ def test_sparse_scatter_offset_outside_path_budget_disqualifies_the_corner():
assert resolved_loc(ax) == "upper left"


def test_datetime_paths_participate_in_best_placement():
"""Datetime vertices use the same converted millisecond space as drawing."""
def test_datetime_text_and_annotations_participate_in_best_placement():
"""Each string-datetime text artist disqualifies one top corner."""
_, ax = plt.subplots()
dates = np.asarray(["2026-01-01", "2026-01-02"], dtype="datetime64[D]")
ax.plot(dates, [0.0, 1.0], label="dated")
ax.plot(dates, [0.5, 0.5], label="dated")
ax.set_ylim(0, 1)
ax.text("2026-1-2", 0.95, "text")
ax.annotate("annotation", xy=("2026-1-1", 0.95))
ax.legend(loc="best")
assert resolved_loc(ax) == "upper left"
assert resolved_loc(ax) == "lower left"


def test_best_is_resolved_before_the_wire():
Expand Down
23 changes: 23 additions & 0 deletions tests/pyplot/test_categorical_gallery_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,29 @@ def test_bar_patch_labels_validate_against_the_bar_count() -> None:
ax.bar(["apple", "orange"], [1, 2], label=["only one"])


def test_bar_materializes_dictionary_views_before_shape_and_autoscale() -> None:
data = {"January": 2.0, "February": 5.0, "March": 3.0}
widths = [0.4, 0.6, 0.8]
bottoms = [1.0, 2.0, 4.0]
_fig, ax = plt.subplots()

bars = ax.bar(
data.keys(),
data.values(),
width=(value for value in widths),
bottom=(value for value in bottoms),
)

entry = ax._entries[0]
assert list(entry["x"]) == list(data)
assert list(entry["y"]) == list(data.values())
np.testing.assert_allclose(entry["kwargs"]["width"], widths)
np.testing.assert_allclose(entry["kwargs"]["base"], bottoms)
np.testing.assert_allclose(bars.tops, np.asarray(bottoms) + list(data.values()))
assert ax.get_xlim()[1] > 1.0
assert ax.get_ylim()[1] >= 7.0


def test_barh_gallery_width_vector_remains_bar_value_geometry() -> None:
people = ("Tom", "Dick", "Harry", "Slim", "Jim")
performance = [5, 7, 6, 4, 9]
Expand Down
21 changes: 21 additions & 0 deletions tests/pyplot/test_p3_option_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,27 @@ def test_contourf_legend_elements_keep_per_band_hatches_and_handleheight() -> No
assert all(item["kind"] == "bar" for item in spec["legend"]["items"])


def test_contourf_preserves_unknown_public_extend_as_unextended_geometry() -> None:
_fig, ax = plt.subplots()
contour = ax.contourf(_Z, levels=4, extend="lower")

assert contour._entry["extend"] == "lower"
assert contour._entry["kwargs"]["extend"] == "neither"


def test_violinplot_flat_quantiles_are_one_single_violin_group() -> None:
_fig, ax = plt.subplots()
result = ax.violinplot(
[1.0, 2.0, 3.0, 4.0],
quantiles=[0.25, 0.5, 0.75],
showextrema=False,
)

quantile_segments = result["cquantiles"]._entry["args"]
assert len(quantile_segments[0]) == 3
np.testing.assert_allclose(quantile_segments[1], [1.75, 2.5, 3.25])


def test_plot_masked_integer_coordinates_use_nan_gaps() -> None:
_fig, ax = plt.subplots()
line = ax.plot(
Expand Down
Loading
Loading