The Wayback Machine - https://web.archive.org/web/20240111234418/https://github.com/matplotlib/matplotlib/issues/25771
Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug]: Slider #25771

Open
pdalet opened this issue Apr 26, 2023 · 4 comments
Open

[Bug]: Slider #25771

pdalet opened this issue Apr 26, 2023 · 4 comments

Comments

@pdalet
Copy link

pdalet commented Apr 26, 2023

Bug summary

I have found a bug with the slider object (matplotlib 3.7.1)

The script is joined.
It works very well with win 10 and ubuntu 22.04 and python3.11.2
It doesn't work with with win 10 and ubuntu 22.04 and python3.11.3

When I click on the slider, I have this message

ax is None
I suppose a bug from python 3.11.3 or matplotlib !!

Thanks a lot

Philippe DALET
Lyp Champollion
FIGEAC - FRANCE

Code for reproduction

#!/usr/local/bin/python3.11
# -*- coding: utf-8 -*-
# Philippe DALET
# Lyp Champollion-46100 FIGEAC
# 22 october 2022
# 20 april 2023

import matplotlib.pyplot as plt
from matplotlib.widgets import Slider
import matplotlib.animation as animation
from math import pi
import numpy as np


xmax=10
Nmax=1000
lamb=3.0
#constants
xA=1
xB=2

#t=0
T=2
v=lamb/T
dt = T/50
Amplitude=1

#datas
x = np.linspace(0, xmax, Nmax)
t = np.linspace(0, 2*T, Nmax)
#camera
y = np.cos( 2*pi*( t/T - x/lamb) )
#oscillogram
yA_t = Amplitude*np.cos( 2*pi*( t/T - xA/lamb) )
yB_t = Amplitude*np.cos( 2*pi*( t/T - xB/lamb) )

#plot
fig, a = plt.subplots(2)
plt.subplots_adjust(left=0.10, bottom=0.25)
fig.suptitle(r'$Onde\ progressive\ sinusoidale$',color='b',fontsize=20)

#camera 
a[0].set_xlim(0,xmax)
a[0].set_xticks(np.arange(0, 10, step=0.5))
a[0].set_ylim(-1.5*Amplitude,+1.5*Amplitude)
a[0].grid(True)
a[0].set_xlabel('x (m)', fontsize=10)
a[0].set_ylabel(r'$y(x,t)$', rotation=0, fontsize=10)
a[0].text(8.5, 1.2, r'$\lambda=%dm\ et\ T=%ds$' %(lamb,T) ,  fontsize=12)


#oscillogramm yA yB
a[1].set_xlim(0,2*T)
a[1].set_ylim(-1.5*Amplitude,+1.5*Amplitude)
a[1].grid(True)
a[1].set_xlabel('t (s)', fontsize=10)
a[1].set_ylabel(r'$y(t)$', rotation=0, fontsize=10)
a[1].plot(t,yA_t,'b', label=r'$y_{xA}(t)$')
a[1].plot(t,yB_t,'g', label=r'$y_{xB}(t)$')
a[1].legend(loc=1)

line, = a[0].plot(x,y,'r')

def init():
    line.set_ydata(np.ma.array(x, mask=True))
    scatA=a[0].scatter(xA, 0 , c = 'b')
    scatB=a[0].scatter(xB, 0 , c = 'g')
    lineA=a[0].vlines(xA, -1.5*Amplitude,+1.5*Amplitude, colors = 'b')
    lineB=a[0].vlines(xB, -1.5*Amplitude,+1.5*Amplitude, colors = 'g')
    return line, scatA, scatB, lineA, lineB

def animate(i):
    global x,y,t,line
    t=i*dt
    y = Amplitude*np.cos( 2*pi*( t/T - x/lamb) )
    yA=y[int(xA*100)]
    yB=y[int(xB*100)]
    line.set_ydata(y)
    scatA=a[0].scatter(xA, yA , c = 'b')
    scatB=a[0].scatter(xB, yB , c = 'g')
    lineA=a[0].vlines(xA, -1.5*Amplitude,+1.5*Amplitude, colors = 'b')
    lineB=a[0].vlines(xB, -1.5*Amplitude,+1.5*Amplitude, colors = 'g')
    return line, scatA, scatB, lineA, lineB

def plot():
    #oscillogramm
    a[1].clear()
    a[1].grid(True)
    a[1].set_xlim(0,2*T)
    a[1].set_ylim(-1.5*Amplitude,+1.5*Amplitude)
    a[1].set_xlabel('t (s)', fontsize=10)
    a[1].set_ylabel(r'$y(t)$', rotation=0, fontsize=10)
    t = np.linspace(0, 2*T, Nmax)
    yA_t = Amplitude*np.cos( 2*pi*( t/T - xA/lamb) )
    yB_t = Amplitude*np.cos( 2*pi*( t/T - xB/lamb) )
    a[1].plot(t,yA_t,'b', label=r'$y_{xA}(t)$')
    a[1].plot(t,yB_t,'g', label=r'$y_{xB}(t)$')
    a[1].legend(loc=1)
    #camera
    a[0].clear()
    a[0].grid(True)
    a[0].set_xlim(0,xmax)
    a[0].set_xlabel('x (m)', fontsize=10)
    a[0].set_ylabel(r'$y(x,t)$', rotation=0, fontsize=10)
    a[0].set_xticks(np.arange(0, 10, step=0.5))
    a[0].set_ylim(-1.5*Amplitude,+1.5*Amplitude)
    fig.canvas.draw_idle()

def updateA(val):
    global xA
    xA=val
    plot()

def updateB(val):
    global xB
    xB=val
    plot()

# horizontal slider to control xA
axA = plt.axes([0.10, 0.15, 0.80, 0.03], facecolor= 'lightgoldenrodyellow')
xA_slider = Slider( ax=axA,label='xA', valmin=0, valmax=xmax, valinit=xA, color='blue')
xA_slider.on_changed(updateA)
# horizontal slider to control xB
axB = plt.axes([0.10, 0.10, 0.80, 0.03], facecolor= 'lightgoldenrodyellow')
xB_slider = Slider( ax=axB,label='xB', valmin=0, valmax=xmax, valinit=xB, color='green')
xB_slider.on_changed(updateB)


ani = animation.FuncAnimation(fig, animate, init_func=init, frames=100 ,interval=25, blit=True)
figManager = plt.get_current_fig_manager()
#figManager.window.showMaximized() # QtAgg only
plt.show()

Actual outcome

Traceback (most recent call last):
File "/usr/local/lib/python3.11/site-packages/matplotlib/backend_bases.py", line 1226, in _on_timer
ret = func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/matplotlib/animation.py", line 1426, in _step
still_going = super()._step(*args)
^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/matplotlib/animation.py", line 1119, in _step
self._draw_next_frame(framedata, self._blit)
File "/usr/local/lib/python3.11/site-packages/matplotlib/animation.py", line 1139, in _draw_next_frame
self._post_draw(framedata, blit)
File "/usr/local/lib/python3.11/site-packages/matplotlib/animation.py", line 1162, in _post_draw
self._blit_draw(self._drawn_artists)
File "/usr/local/lib/python3.11/site-packages/matplotlib/animation.py", line 1177, in _blit_draw
cur_view = ax._get_view()
^^^^^^^^^^^^

AttributeError: 'NoneType' object has no attribute '_get_view'
Aborted (core dumped)

Expected outcome

crash

Additional information

No response

Operating system

windows 10 , Ubuntu 22.04

Matplotlib Version

3.7.1

Matplotlib Backend

QtAgg

Python version

3.11.3

Jupyter version

No response

Installation

pip

@ksunden
Copy link
Member

ksunden commented Apr 26, 2023

The slider is a bit of a red herring here, the actual problem is that line is being removed from your Axes by ax[0].clear() in def plot(). Thus it is only affected when plot is called, which only happens on slider updates.

This can be worked around by doing ax[0].add_line(line) after the clear in plot (though re-adding artists can be a bit of a hazard, still) or by removing the call to clear (as its not even clear to me why that was needed in the first place).

If I add code to ignore that we are being asked to use that line, then the red line disappears when you move the slider (which is correct, as the axes were cleared, and "animate" does not re-add that line (though it does update the ydata).

Since it is an artist which is returned by your animate function, it is passed into the internal animation code (specifically regarding blitting).

It is unclear to me whether we should be a bit more defensive to avoid erroring if an artist is removed from axes

@ksunden
Copy link
Member

ksunden commented Apr 26, 2023

I lean towards saying "It is unexpected and indicative of an underlying error that an artist which has been removed to be returned from my animation function" and thus current behave should remain.

But if we do decide to be a bit more defensive, the following diff accomplishes that:

diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py
index 08f1075fb4..da648e333a 100644
--- a/lib/matplotlib/animation.py
+++ b/lib/matplotlib/animation.py
@@ -1156,6 +1156,7 @@ class Animation:
         # Handles blitted drawing, which renders only the artists given instead
         # of the entire figure.
         updated_ax = {a.axes for a in artists}
+        updated_ax -= {None}
         # Enumerate artists to cache Axes backgrounds. We do not draw
         # artists yet to not cache foreground from plots with shared axes
         for ax in updated_ax:
@@ -1169,7 +1170,8 @@ class Animation:
                     cur_view, ax.figure.canvas.copy_from_bbox(ax.bbox))
         # Make a separate pass to draw foreground.
         for a in artists:
-            a.axes.draw_artist(a)
+            if a.axes is not None:
+                a.axes.draw_artist(a)
         # After rendering all the needed artists, blit each Axes individually.
         for ax in updated_ax:
             ax.figure.canvas.blit(ax.bbox)

@tacaswell
Copy link
Member

Given the axes-centric ness of the animation code, I'm inclined to say that we should still error, but just a more understandable error. Probably at the places where Kyle's patch hits.

@ksunden
Copy link
Member

ksunden commented Apr 26, 2023

More minimal reproducer, which strips out the Slider callback by just calling plot() directly

import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np

N=1000
x = np.linspace(0, 1, N)
y = np.linspace(0, 1, N)

#plot
fig, ax = plt.subplots()

line, = ax.plot(x,y)

def init():
    line.set_ydata(y)
    return line,

def animate(i):
    line.set_ydata(y * i)
    return line,

def plot():
    #camera
    ax.clear()
    #ax.add_line(line)

ani = animation.FuncAnimation(fig, animate, init_func=init, frames=100 ,interval=25, blit=True)

# Note calling plot here, instead of by a Slider
plot()
plt.show()

Note that if you turn blitting off it does not crash, but nothing is drawn since the line is removed. (Well, and simply FuncAnimation ignores the return of animate entirely, so probably nothing to do in the non-blit case)

I'd propose to put the check a little earlier in the call stack, something like here, where we already do some raising of exceptions if unexpected values are returned:

if self._blit:
err = RuntimeError('The animation function must return a sequence '
'of Artist objects.')
try:
# check if a sequence
iter(self._drawn_artists)
except TypeError:
raise err from None
# check each item if it's artist
for i in self._drawn_artists:
if not isinstance(i, mpl.artist.Artist):
raise err

Not sure it would resolve the segfault though, as that seems to be backend dependent and hard to ensure we don't do so:

  • QtAgg (PyQt6) errors, exits, but not a segfault
  • QtAgg (PySide6) errors many times, but does not exit or close the window
  • TkAgg errors exactly twice, but does not exit/close the window
  • Gtk4Agg/webagg don't actually error at all?? (rather confused about this one)

Perhaps that is actually the underlying change, but regardless I think the red line from the original report would be missing, even if the animation continued without segfaulting/exception.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants