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?
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.