From ae7f7697b264d204850e08a06e36eb53dccfd637 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 2 Apr 2024 14:06:46 +0200 Subject: [PATCH 1/2] Make Grouper return siblings in the order in which they have been seen. --- lib/matplotlib/cbook.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index ee043f351dbb..a41bfe56744f 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -849,12 +849,18 @@ class Grouper: def __init__(self, init=()): self._mapping = weakref.WeakKeyDictionary( {x: weakref.WeakSet([x]) for x in init}) + self._ordering = weakref.WeakKeyDictionary() + for x in init: + if x not in self._ordering: + self._ordering[x] = len(self._ordering) + self._next_order = len(self._ordering) # Plain int to simplify pickling. def __getstate__(self): return { **vars(self), # Convert weak refs to strong ones. "_mapping": {k: set(v) for k, v in self._mapping.items()}, + "_ordering": {**self._ordering}, } def __setstate__(self, state): @@ -862,6 +868,7 @@ def __setstate__(self, state): # Convert strong refs to weak ones. self._mapping = weakref.WeakKeyDictionary( {k: weakref.WeakSet(v) for k, v in self._mapping.items()}) + self._ordering = weakref.WeakKeyDictionary(self._ordering) def __contains__(self, item): return item in self._mapping @@ -875,10 +882,19 @@ def join(self, a, *args): Join given arguments into the same set. Accepts one or more arguments. """ mapping = self._mapping - set_a = mapping.setdefault(a, weakref.WeakSet([a])) - + try: + set_a = mapping[a] + except KeyError: + set_a = mapping[a] = weakref.WeakSet([a]) + self._ordering[a] = self._next_order + self._next_order += 1 for arg in args: - set_b = mapping.get(arg, weakref.WeakSet([arg])) + try: + set_b = mapping[arg] + except KeyError: + set_b = mapping[arg] = weakref.WeakSet([arg]) + self._ordering[arg] = self._next_order + self._next_order += 1 if set_b is not set_a: if len(set_b) > len(set_a): set_a, set_b = set_b, set_a @@ -892,9 +908,8 @@ def joined(self, a, b): def remove(self, a): """Remove *a* from the grouper, doing nothing if it is not there.""" - set_a = self._mapping.pop(a, None) - if set_a: - set_a.remove(a) + self._mapping.pop(a, {a}).remove(a) + self._ordering.pop(a, None) def __iter__(self): """ @@ -904,12 +919,12 @@ def __iter__(self): """ unique_groups = {id(group): group for group in self._mapping.values()} for group in unique_groups.values(): - yield [x for x in group] + yield sorted(group, key=self._ordering.__getitem__) def get_siblings(self, a): """Return all of the items joined with *a*, including itself.""" siblings = self._mapping.get(a, [a]) - return [x for x in siblings] + return sorted(siblings, key=self._ordering.get) class GrouperView: From c5b2158e0d354b632d9ec4158cf25153fb267811 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 26 Mar 2023 23:45:00 +0200 Subject: [PATCH 2/2] Display cursor coordinates for all axes twinned with the current one. --- lib/matplotlib/axes/_base.py | 18 ++++++++++++++---- lib/matplotlib/tests/test_backend_bases.py | 20 ++++++++++++++------ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 36baff85fa66..23cc1c869c07 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -3976,10 +3976,20 @@ def format_ydata(self, y): def format_coord(self, x, y): """Return a format string formatting the *x*, *y* coordinates.""" - return "x={} y={}".format( - "???" if x is None else self.format_xdata(x), - "???" if y is None else self.format_ydata(y), - ) + twins = self._twinned_axes.get_siblings(self) + if len(twins) == 1: + return "(x, y) = ({}, {})".format( + "???" if x is None else self.format_xdata(x), + "???" if y is None else self.format_ydata(y)) + screen_xy = self.transData.transform((x, y)) + xy_strs = [] + # Retrieve twins in the order of self.figure.axes to sort tied zorders (which is + # the common case) by the order in which they are added to the figure. + for ax in sorted(twins, key=attrgetter("zorder")): + data_x, data_y = ax.transData.inverted().transform(screen_xy) + xy_strs.append( + "({}, {})".format(ax.format_xdata(data_x), ax.format_ydata(data_y))) + return "(x, y) = {}".format(" | ".join(xy_strs)) def minorticks_on(self): """ diff --git a/lib/matplotlib/tests/test_backend_bases.py b/lib/matplotlib/tests/test_backend_bases.py index 399949c93bef..c264f01acdb2 100644 --- a/lib/matplotlib/tests/test_backend_bases.py +++ b/lib/matplotlib/tests/test_backend_bases.py @@ -1,5 +1,3 @@ -import re - from matplotlib import path, transforms from matplotlib.backend_bases import ( FigureCanvasBase, KeyEvent, LocationEvent, MouseButton, MouseEvent, @@ -123,11 +121,21 @@ def test_location_event_position(x, y): assert event.y == int(y) assert isinstance(event.y, int) if x is not None and y is not None: - assert re.match( - f"x={ax.format_xdata(x)} +y={ax.format_ydata(y)}", - ax.format_coord(x, y)) + assert (ax.format_coord(x, y) + == f"(x, y) = ({ax.format_xdata(x)}, {ax.format_ydata(y)})") ax.fmt_xdata = ax.fmt_ydata = lambda x: "foo" - assert re.match("x=foo +y=foo", ax.format_coord(x, y)) + assert ax.format_coord(x, y) == "(x, y) = (foo, foo)" + + +def test_location_event_position_twin(): + fig, ax = plt.subplots() + ax.set(xlim=(0, 10), ylim=(0, 20)) + assert ax.format_coord(5., 5.) == "(x, y) = (5.00, 5.00)" + ax.twinx().set(ylim=(0, 40)) + assert ax.format_coord(5., 5.) == "(x, y) = (5.00, 5.00) | (5.00, 10.0)" + ax.twiny().set(xlim=(0, 5)) + assert (ax.format_coord(5., 5.) + == "(x, y) = (5.00, 5.00) | (5.00, 10.0) | (2.50, 5.00)") def test_pick():