Search code examples
pythonmatplotlibannotationsmatplotlib-widget

Annotation object handling in matplotlib


I am trying to implement a plot with interactive sliders in Matplotlib. I used the suggestions from Interactive matplotlib plot with two sliders to implement the sliders. Now I am trying to move an annotation of a point when the sliders change. There appears to be some strange behaviour that I can not figure out.

Below is a working example code. The code produces a point in the 3rd subplot and when the slider is changed the point moves. There is an annotation on the point that gets moved with the point. However in the current example old annotations are not removed, which is unwanted.

I tried some things to fix this. E.g. I would have expected the commented code marked with '!!' to do the job: remove the old annotation and then a new one is added afterwards. However if that is uncommented, nothing happens to the annotation at all when the slider is changed.

I have read Remove annotation while keeping plot matplotlib, Python and Remove annotation from figure and Remove and Re-Add Object in matplotlib | Toggle Object appearance matplotlib. I tried to implement the suggestions from there, i.e. to change the coordinates of the annotation or to add the annotation as an artist object. Non of the two did anything.

Is there something fundamental I'm getting wrong here? I feel like I don't understand how the object variables for annotations are handled in python.

One more comment: I would prefer to not change the structure of the code too much (especially the sympy use). I removed a lot of things to create this minimal example to reproduce the error, but I need the structure for my other plots.

import numpy as np
import sympy as sp
import matplotlib.pyplot as plt
import matplotlib
from matplotlib.widgets import Slider, Button, RadioButtons

## Define variable symbols ##
var1 = sp.Symbol('var1')

## Define initial value ##
var1_init = 5

## Marker position functions ##
def positionY(var1):
    return var1

def positionX(var1):
    return var1

## plot ##
axis_color = 'lightgoldenrodyellow'

fig = plt.figure()
ax = fig.add_subplot(131)
ax2 = fig.add_subplot(132)
ax3 = fig.add_subplot(133)

# Adjust the subplots region to leave some space for the slider
fig.subplots_adjust(left=0.25, bottom=0.3)

# Draw the initial plot
marker_coord = (positionX(var1_init),positionY(var1_init))
[qMarker1] = ax3.plot(marker_coord[0],marker_coord[1],'ro')
ax3.set_xlim([-1.0, 11.0])
ax3.set_ylim([-1.0, 11.0])
qAnnotation = ax3.annotate('(%.2f, %.2f)' % marker_coord, xy=marker_coord, textcoords='data')

## Add sliders ##

# Define an axes area and draw sliders in it
var1_slider_ax  = fig.add_axes([0.25, 0.2, 0.65, 0.03], facecolor=axis_color)
var1_slider = Slider(var1_slider_ax, 'var1', 0, 10.0, valinit=var1_init)

# Define an action for modifying the plot1 when any slider's value changes
def sliders_on_changed(val):
    qMarker1.set_ydata(positionY(var1_slider.val))
    qMarker1.set_xdata(positionX(var1_slider.val))
    #qAnnotation.remove() ## <--------------------------- !!
    marker_coord = (positionX(var1_slider.val),positionY(var1_slider.val))
    qAnnotation = ax3.annotate('(%.2f, %.2f)' % marker_coord, xy=marker_coord, textcoords='data')
    fig.canvas.draw_idle()
var1_slider.on_changed(sliders_on_changed)

# Add a button for resetting the parameters
reset_button_ax = fig.add_axes([0.05, 0.1, 0.1, 0.04])
reset_button = Button(reset_button_ax, 'Reset', color=axis_color, hovercolor='0.975')
def reset_button_on_clicked(mouse_event):
    var1_slider.reset()
reset_button.on_clicked(reset_button_on_clicked)

plt.show()

Solution

  • Uncommenting the line qAnnotation.remove() produces an error UnboundLocalError: local variable 'qAnnotation' referenced before assignment, which is pretty explanatory on the problem. The qAnnotation is redefined in the local scope of the function. So it's really about understanding local and global scopes in python and has nothing to do with specific matplotlib objects.

    The error can be easily reproduced in a case like

    a = 0
    def f():
        a += 1
        a=100
    f()
    

    which also throws, UnboundLocalError: local variable 'a' referenced before assignment.

    Easiest solution: Make qAnnotation available in the global scope, global qAnnotation.

    def sliders_on_changed(val):
        global qAnnotation
        # ..
        qAnnotation.remove() 
        qAnnotation = ax3.annotate(...)
        fig.canvas.draw_idle()
    

    A different solution avoiding explicit global statements would be to make the annotation be part of a list and access this list locally.

    qAnnotation = [ax3.annotate( ... )]
    
    def sliders_on_changed(val):
        # ..
        qAnnotation[0].remove()
        qAnnotation[0] = ax3.annotate( ... )
        fig.canvas.draw_idle()