0

I have seen Possible to make labels appear when hovering over a point in matplotlib? - but unfortunately, it does not help me with this specific case.

Consider this example:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import matplotlib
print("matplotlib.__version__ {}".format(matplotlib.__version__))
import matplotlib.pyplot as plt
import numpy as np


def onhover(event, fig, axes):
  print(event)

def main():
  xdata = np.arange(0, 101, 1) # 0 to 100, both included
  ydata1 = np.sin(0.01*xdata*np.pi/2)

  fig, ax1 = plt.subplots(1, 1, figsize=(9, 6), dpi=120)
  fig.subplots_adjust(hspace=0)

  pl11, = ax1.plot(xdata, ydata1, color="Red", label="My plot")

  leg = ax1.legend(ncol=1, bbox_to_anchor=(0,1.01), loc="lower left", borderaxespad=0, prop={'size': 8})

  fig.canvas.mpl_connect('motion_notify_event', lambda event: onhover(event, fig, (ax1,) ))
  plt.show()

# ENTRY POINT
if __name__ == '__main__':
  main()

This results with the following plot:

mpl_plot.png

The thing is: if I move the mouse pointer, so it hovers over the legend entry, all I get as the printout in the event is:

....
motion_notify_event: xy=(175, 652) xydata=(None, None) button=None dblclick=False inaxes=None
motion_notify_event: xy=(174, 652) xydata=(None, None) button=None dblclick=False inaxes=None
motion_notify_event: xy=(173, 652) xydata=(None, None) button=None dblclick=False inaxes=None
motion_notify_event: xy=(172, 652) xydata=(None, None) button=None dblclick=False inaxes=None

... which makes sense, as I've deliberately placed the legend outside of the plot.

However, now I do not know how I can get a reference to the legend entry? What I would like to do, is basically get the reference to the legend entry, so that I could write the same text as the legend label (here "My plot") in a "tooltip" (that is, in this case, Matplotlib annotation) followed by some other text; and then, once the mouse leaves the region of the legend entry, the tooltip/annotation should disappear.

Can I achieve this with Matplotlib - and if so, how?

sdbbs
  • 4,270
  • 5
  • 32
  • 87

2 Answers2

2

You could do something like that:

def onhover(event):
    if leg.get_window_extent().contains(event.x,event.y):
        print(event, "In legend! do something!")


xdata = np.arange(0, 101, 1) # 0 to 100, both included
ydata1 = np.sin(0.01*xdata*np.pi/2)

fig, ax1 = plt.subplots(1, 1, figsize=(9, 6), dpi=120)
pl11, = ax1.plot(xdata, ydata1, color="Red", label="My plot")
leg = ax1.legend(ncol=1, bbox_to_anchor=(0,1.01), loc="lower left", borderaxespad=0, prop={'size': 8})

fig.canvas.mpl_connect('motion_notify_event', onhover)
plt.show()
Diziet Asahi
  • 38,379
  • 7
  • 60
  • 75
0

Here is a complete rework of the OP, with two plots and two legend entries, with actual hovering - a little complicated to achieve, as there are coordinate systems transformations and z order to take into account as well:

mpl_plot2.png

Here's the code - including comments:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import matplotlib
print("matplotlib.__version__ {}".format(matplotlib.__version__))
import matplotlib.pyplot as plt
import numpy as np


def onhover(event, fig, axes, leg, tooltip):
  if leg.get_window_extent().contains(event.x,event.y):
    print(event, "In legend! do something!")
    ax1 = axes[0]
    hand, labl = ax1.get_legend_handles_labels()
    #print(hand)
    #print(labl)
    r = plt.gcf().canvas.get_renderer()
    #for ilidx, ilabl in enumerate(labl): # labl contents are just str! so no geometry data
    #  print(ilabl.get_window_extent())
    #for ihidx, ihand in enumerate(hand):
    #  # NOTE: just ihand.get_window_extent() causes: TypeError: get_window_extent() missing 1 required positional argument: 'renderer'
    #  print(ihand.get_window_extent(r)) # apparently, this is the original line, not just the legend line, as here it prints: Bbox(x0=173.04545454545456, y0=104.10999999999999, x1=933.9545454545455, y1=606.71)
    #  if ihand.get_window_extent(r).contains(event.x,event.y):
    #    print("EEEE")
    hoveredtxt = None
    for itxt in leg.get_texts():
      #print(itxt)
      #print(itxt.get_window_extent().contains(event.x,event.y))
      if itxt.get_window_extent().contains(event.x,event.y):
        #print("Legend hover on: {}".format(itxt))
        hoveredtxt = itxt
        break
    if hoveredtxt is not None:
      tooltip.set_text("'{}' is hovered!".format(hoveredtxt.get_text()))
      tooltip.set_visible(True)
      # see: https://matplotlib.org/3.1.1/gallery/recipes/placing_text_boxes.html
      # https://matplotlib.org/3.1.1/tutorials/advanced/transforms_tutorial.html - None for "display" coord system
      tooltip.set_transform( None )
      tooltip.set_position( (event.x, event.y) ) # is by default in data coords!
    else:
      tooltip.set_visible(False)
    fig.canvas.draw_idle()


def main():
  xdata = np.arange(0, 101, 1) # 0 to 100, both included
  ydata1 = np.sin(0.01*xdata*np.pi/2)
  ydata2 = 10*np.sin(0.01*xdata*np.pi/4)

  fig, ax1 = plt.subplots(1, 1, figsize=(9, 6), dpi=120)
  fig.subplots_adjust(hspace=0)

  pl11, = ax1.plot(xdata, ydata1, color="Red", label="My plot")
  pl12, = ax1.plot(xdata, ydata2, color="Blue", label="Other stuff")

  leg = ax1.legend(ncol=2, bbox_to_anchor=(0,1.01), loc="lower left", borderaxespad=0, prop={'size': 8})

  # NOTE: ax.annotate without an arrow is basically ax.text;
  #tooltip = ax.annotate("", xy=(0, 0), xytext=(-20, 20), textcoords="offset points",
  #                    bbox=dict(boxstyle="round", fc="w"),
  #                    arrowprops=dict(arrowstyle="->"))
  # however, no `textcoords` in .text; "The default transform specifies that text is in data coords, alternatively, you can specify text in axis coords", via transform=ax.transAxes
  # must add zorder of high number too, else the tooltip comes under the legend!
  tooltip = ax1.text(0, 0, 'TEST', bbox=dict(boxstyle="round", fc="w"), zorder=10)
  tooltip.set_visible(False)

  fig.canvas.mpl_connect('motion_notify_event', lambda event: onhover(event, fig, (ax1,), leg, tooltip ))
  plt.show()

# ENTRY POINT
if __name__ == '__main__':
  main()
sdbbs
  • 4,270
  • 5
  • 32
  • 87