While working on the draft for the next – probable – post, I realized I wanted to have two annotations visible on a chart at one time so that we could compare the values of the bars more precisely. With the current code for displaying the annotations on a “mouseover event” that just wasn’t possible. I tried using the current callback generator, cb_create(), for the call to “fig.canvas.mpl_connect()” with a “button_press_event”. But that still didn’t work because the call back uses a single Annotation instance. There was no way to have two of them on the chart at one time.

So, I decided to write a new callback generator specifically for mouse clicks. I started by copying cb_create() to cb_click() and removing the section that turned of display of the annotation if the mouse was not hovering over a bar. Then added a new call to plot_1ma() to monitor click events. Well it half worked. The annotation stayed there. But only until I clicked elsewhare. Looking at the code, I recalled that we were using a single annotation for hover events (via a closure), and updating it, displaying or not displaying it depending on the mouse movement. So, I moved code creating the annotation into the callback function so that each click created a new one. I should likely have made sure that there wasn’t alreay an annotation for the bar clicked but was a little too lazy.

(base) PS R:\learn\py_play> conda activate base-3.8
(base-3.8) PS R:\learn\py_play> python.exe r:/learn/py_play/population/chart/chart.py -t 1
args.nbr_yrs 2

chart data: [['China'], ['2010', 2], ['all']]

And that appeared to work. So now I could hover a bar to see it’s details, and if I clicked it the annotation would remain in place. And, I could have multiple annotations open at one time. Now, how to get rid of them? chart showing multiple annotations being displayed at one time

And my code:

def cb_click(ax, fig, bar_cont, plot_for, lgnds, x_lbls):
  """Create callback for click event. Annotation to stay visible until it is clicked
     See also: op_click(event)
     Need to create new annotation for each click, unlike the mouse hover effect
     Issues: var 'ax' not available to op_click(), so updating chart in callback(), not ideal!
  """

  def callback(event):
    annot = ax.annotate(f"", xy=(0,0), xytext=(-5,20), textcoords="offset points", color='white', zorder=99.9,
                        bbox=dict(boxstyle="round", fc="black", ec="b", lw=2, alpha=0.8),
                        arrowprops=dict(arrowstyle="->"), picker=True)
    annot.set_visible(False)
    
    is_small_bar = False
    is_visible = annot.get_visible()
    
    i = 0
    for grp_cont in bar_cont:
      j = 0
      for p_bar in grp_cont:
        if event.inaxes == ax:
          cont, _ = p_bar.contains(event)
          if not cont:
            is_small_bar = (event.xdata >= p_bar.get_x()) and (event.xdata < (p_bar.get_x() + p_bar.get_width())) and (event.y <= 100)
          if cont or is_small_bar:
            b_ht = p_bar.get_height()
            x = p_bar.get_x()+p_bar.get_width()/2.
            y = p_bar.get_y()+b_ht
            annot.xy = (x,y)
            text = f"{plot_for}: {lgnds[i]}\n{x_lbls[j]}\n{b_ht:.3f}"
            if do_percent:
              text += " %"
            else:
              text += " (000s)"
            annot.set_text(text)
            annot.set_visible(True)
            fig.canvas.draw_idle()
            return
        j += 1
      i += 1
  return callback
  
  # and in plot_ima()
  onmover = cb_create(ax, fig, c_rects, p_nms[0], list(p_years[0].keys()), x_lbls)
  mo_id = fig.canvas.mpl_connect('motion_notify_event', onmover)
  onclick = cb_click(ax, fig, c_rects, p_nms[0], list(p_years[0].keys()), x_lbls)
  c_id = fig.canvas.mpl_connect('button_press_event', onclick)

I settled for clicking on an annotation to have it removed. Turns out I needed to use the compatriot of ’event handling’ — ‘picking’. See Event handling and picking.

So, I needed another function to generate the callback for the “picker”, i.e. the pick event. I started with just a simple function, no closures involved.

def op_click(event):
  if isinstance(event.artist, Annotation):
    event.artist.remove()

But I got an error related to the check that the mouse had clicked on an annotation to work.

Traceback (most recent call last):
  File "E:\appDev\Miniconda3\envs\base-3.8\lib\site-packages\matplotlib\cbook\__init__.py", line 216, in process
    func(*args, **kwargs)
  File "r:/learn/py_play/population/chart/chart.py", line 179, in op_click
    if isinstance(event.artist, Annotation):
NameError: name 'Annotation' is not defined

I had to import the Annotation class code to get that line to work. So at the top of the file I added:

from matplotlib.text import Annotation

But, it turned out I didn’t have anyway in that function to update the chart after removing the annotation I had clicked on. So, it stayed there until one of my other two event handlers updated the chart. Didn’t like that, used a closure and returned a suitable callback function.

def pick_create(fig):

  def op_click(event):
    if isinstance(event.artist, Annotation):
      event.artist.remove()
      fig.canvas.draw_idle()

  return op_click

# Now plot_1ma() had the following event related code like:
  ...
  onmover = cb_create(ax, fig, c_rects, p_nms[0], list(p_years[0].keys()), x_lbls)
  mo_id = fig.canvas.mpl_connect('motion_notify_event', onmover)
  onclick = cb_click(ax, fig, c_rects, p_nms[0], list(p_years[0].keys()), x_lbls)
  c_id = fig.canvas.mpl_connect('button_press_event', onclick)
  p_id = fig.canvas.mpl_connect('pick_event', pick_create(fig))
  ...

And, I pretty much had what I wanted. Don’t know the code is all that tidy, or that there isn’t a way to combine some of the stuff into individual functions; but…

Now, back to the post I was working on before going on this side trip.

Resources