diff --git a/galleries/examples/event_handling/hover_event_demo.py b/galleries/examples/event_handling/hover_event_demo.py index 5460a9913fe8..cfa34a4c510b 100644 --- a/galleries/examples/event_handling/hover_event_demo.py +++ b/galleries/examples/event_handling/hover_event_demo.py @@ -19,6 +19,9 @@ * list of string literals - hovering is enabled, and hovering over a point displays the corresponding string literal. +* dictionary - hovering is enabled, and hovering over a point + displays the string literal corresponding to the coordinate tuple. + * function - if hover is callable, it is a user supplied function which takes a ``mouseevent`` object (see below), and returns a tuple of transformed coordinates @@ -69,16 +72,26 @@ def hover_handler(event): from numpy.random import rand fig, ax = plt.subplots() -plt.ylabel('some numbers') ax.plot(rand(3), 'o', hover=['London', 'Paris', 'Barcelona']) plt.show() +# %% +# Hover with dictionary data +# -------------------------------- +fig, ax = plt.subplots() +x = rand(3) +y = rand(3) +ax.plot(x, y, 'o', hover={ + (x[0], y[0]): "London", + (x[1], y[1]): "Paris", + (x[2], y[2]): "Barcelona"}) +plt.show() + # %% # Hover with a callable transformation function # --------------------------------------------- fig, ax = plt.subplots() -plt.ylabel('some numbers') def user_defined_function(event): diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 32bb280c86c7..69750c5f7700 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -653,10 +653,18 @@ def set_hover(self, hover): function which sets the hover message to be displayed. - A list: If hover is a list of string literals, each string represents - an additional information assigned to each data point. These arbitrary - data labels will appear as a tooltip in the bottom right hand corner + an additional information assigned to each data point. These data labels + will appear as a tooltip in the bottom right hand corner of the screen when the cursor is detected to be hovering over a data - point that corresponds with one of the data labels. + point that corresponds with the data labels in the same order that + was passed in. + + - A dictionary: If hover is a dictionary of key value paris, each key + represents a tuple of x and y coordinate and each value represnets + additional information assigned to each data point. These data labels + will appear as a tooltip in the bottom right hand corner of the + screen when the cursor is detected to be hovering over a data point + that corresponds with one of the data labels. """ self._hover = hover diff --git a/lib/matplotlib/artist.pyi b/lib/matplotlib/artist.pyi index 997d84b08631..dbdb313433c4 100644 --- a/lib/matplotlib/artist.pyi +++ b/lib/matplotlib/artist.pyi @@ -83,11 +83,12 @@ class Artist: hover: None | bool | list[str] + | dict[tuple[float, float], str] | Callable[[Artist, MouseEvent], tuple[bool, dict[Any, Any]]], ) -> None: ... def get_hover( self, - ) -> None | bool | list[str] | Callable[ + ) -> None | bool | list[str] | dict[tuple[float, float], str] | Callable[ [Artist, MouseEvent], tuple[bool, dict[Any, Any]] ]: ... def get_url(self) -> str | None: ... diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 4d15ecda8bd7..d175564e5865 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3015,25 +3015,26 @@ def _nonrect(self, x): return not isinstance(x, Rectangle) def _tooltip_list(self, event, hover): - import matplotlib.pyplot as plt - lines = plt.gca().get_lines() - num_of_points = 0 - for line in lines: - num_of_points += 1 - if num_of_points >= len(hover): + lines = self.canvas.figure.gca().get_lines()[0] + coor_data = list(zip(lines.get_xdata(), lines.get_ydata())) + + if len(coor_data) != len(hover): raise ValueError("""Number of data points - does not match up with number of labels""") + does not match up with number of labels""") else: - mouse_x = event.xdata - mouse_y = event.ydata - for line in lines: - x_data = line.get_xdata() - y_data = line.get_ydata() - for i in range(len(x_data)): - distance = ((event.xdata - x_data[i])**2 - + (event.ydata - y_data[i])**2)**0.5 - if distance < 0.05: - return "Data Label: " + hover[i] + distances = [] + for a in coor_data: + distances.append(((event.xdata - a[0])**2 + + (event.ydata - a[1])**2)**0.5) + if (min(distances) < 0.05): + return f"Data Label: {hover[distances.index(min(distances))]}" + + def _tooltip_dict(self, event, hover): + distances = {} + for a in hover.keys(): + distances[a] = ((event.xdata - a[0])**2 + (event.ydata - a[1])**2)**0.5 + if (min(distances.values()) < 0.05): + return f"Data Label: {hover[min(distances, key=distances.get)]}" def mouse_move(self, event): self._update_cursor(event) @@ -3050,6 +3051,8 @@ def mouse_move(self, event): self.set_hover_message(hover(event)) elif type(hover) == list: self.set_hover_message(self._tooltip_list(event, hover)) + elif type(hover) == dict: + self.set_hover_message(self._tooltip_dict(event, hover)) else: self.set_hover_message(self._mouse_event_to_message(event)) else: diff --git a/lib/matplotlib/backends/_backend_gtk.py b/lib/matplotlib/backends/_backend_gtk.py index e33813e5d3df..3a79e6d88942 100644 --- a/lib/matplotlib/backends/_backend_gtk.py +++ b/lib/matplotlib/backends/_backend_gtk.py @@ -274,6 +274,10 @@ def set_message(self, s): escaped = GLib.markup_escape_text(s) self.message.set_markup(f'{escaped}') + def set_hover_message(self, s): + escaped = GLib.markup_escape_text(s) + self.hover_message.set_markup(f'{escaped}') + def draw_rubberband(self, event, x0, y0, x1, y1): height = self.canvas.figure.bbox.height y1 = height - y1 diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index d6acd5547b85..a878b023242f 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -395,6 +395,11 @@ def __init__(self, toolmanager): self._message = Gtk.Label() self._message.set_justify(Gtk.Justification.RIGHT) self.pack_end(self._message, False, False, 0) + + self.hover_message = Gtk.Label() + self.hover_message.set_justify(Gtk.Justification.RIGHT) + self.pack_end(self.hover_message, False, False, 0) + self.show_all() self._groups = {} self._toolitems = {} @@ -465,6 +470,9 @@ def _add_separator(self): def set_message(self, s): self._message.set_label(s) + def set_hover_message(self, s): + self._hover_message.set_label(s) + @backend_tools._register_tool_class(FigureCanvasGTK3) class SaveFigureGTK3(backend_tools.SaveFigureBase): diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py index cb4006048d55..a6eab78e55b2 100644 --- a/lib/matplotlib/backends/backend_gtk4.py +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -328,6 +328,10 @@ def __init__(self, canvas): self.message.set_justify(Gtk.Justification.RIGHT) self.append(self.message) + self.hover_message = Gtk.Label() + self.hover_message.set_justify(Gtk.Justification.RIGHT) + self.append(self.hover_message) + _NavigationToolbar2GTK.__init__(self, canvas) def save_figure(self, *args): @@ -493,6 +497,9 @@ def _add_separator(self): def set_message(self, s): self._message.set_label(s) + def set_hover_message(self, s): + self._hover_message.set_label(s) + @backend_tools._register_tool_class(FigureCanvasGTK4) class SaveFigureGTK4(backend_tools.SaveFigureBase): diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index 5c83cbbd9fa7..b72e4b23f5af 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -677,9 +677,19 @@ def __init__(self, canvas, parent=None, coordinates=True): _enum("QtWidgets.QSizePolicy.Policy").Expanding, _enum("QtWidgets.QSizePolicy.Policy").Ignored, )) + self.hover_message = QtWidgets.QLabel("", self) + self.hover_message.setAlignment(QtCore.Qt.AlignmentFlag( + _to_int(_enum("QtCore.Qt.AlignmentFlag").AlignRight) | + _to_int(_enum("QtCore.Qt.AlignmentFlag").AlignVCenter))) + self.hover_message.setSizePolicy(QtWidgets.QSizePolicy( + _enum("QtWidgets.QSizePolicy.Policy").Expanding, + _enum("QtWidgets.QSizePolicy.Policy").Ignored, + )) + labelActionHover = self.addWidget(self.hover_message) + labelActionHover.setVisible(True) + labelAction = self.addWidget(self.locLabel) labelAction.setVisible(True) - NavigationToolbar2.__init__(self, canvas) def _icon(self, name): @@ -756,6 +766,11 @@ def set_message(self, s): if self.coordinates: self.locLabel.setText(s) + def set_hover_message(self, s): + self.message.emit(s) + if self.coordinates: + self.hover_message.setText(s) + def draw_rubberband(self, event, x0, y0, x1, y1): height = self.canvas.figure.bbox.height y1 = height - y1 diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index 70f0c0fff515..c6987eb6b8b6 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -1051,6 +1051,8 @@ def __init__(self, canvas, coordinates=True, *, style=wx.TB_BOTTOM): self.AddStretchableSpace() self._label_text = wx.StaticText(self, style=wx.ALIGN_RIGHT) self.AddControl(self._label_text) + self._hover_message = wx.StaticText(self, style=wx.ALIGN_LEFT) + self.AddControl(self._hover_message) self.Realize() @@ -1143,6 +1145,10 @@ def set_message(self, s): if self._coordinates: self._label_text.SetLabel(s) + def set_hover_message(self, s): + if self._coordinates: + self._hover_message.SetLabel(s) + def set_history_buttons(self): can_backward = self._nav_stack._pos > 0 can_forward = self._nav_stack._pos < len(self._nav_stack._elements) - 1 @@ -1163,6 +1169,10 @@ def __init__(self, toolmanager, parent=None, style=wx.TB_BOTTOM): self._space = self.AddStretchableSpace() self._label_text = wx.StaticText(self, style=wx.ALIGN_RIGHT) self.AddControl(self._label_text) + + self._hover_message = wx.StaticText(self, style=wx.ALIGN_LEFT) + self.AddControl(self._hover_message) + self._toolitems = {} self._groups = {} # Mapping of groups to the separator after them. @@ -1240,6 +1250,9 @@ def remove_toolitem(self, name): def set_message(self, s): self._label_text.SetLabel(s) + def set_hover_message(self, s): + self._hover_message.SetLabel(s) + @backend_tools._register_tool_class(_FigureCanvasWxBase) class ConfigureSubplotsWx(backend_tools.ConfigureSubplotsBase):