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

Inconsistent error when plotting with FuncAnimation vs plt.plot #19667

Open
jungerm2 opened this issue Mar 8, 2021 · 4 comments
Open

Inconsistent error when plotting with FuncAnimation vs plt.plot #19667

jungerm2 opened this issue Mar 8, 2021 · 4 comments

Comments

@jungerm2
Copy link

jungerm2 commented Mar 8, 2021

Bug report

Bug summary

The error thrown when there's a data mismatch when animating is not as expected, it doesn't get caught by matplotlib and instead numpy throws one. I was wondering if it would be possible to improve this error message and make it more informative. I wouldn't mind contributing to this small "quality of life" improvement, I'm just not sure where to start or if this is in fact an issue.

Code for reproduction

This error can be reproduced with the following code:

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from IPython.display import HTML

plt.rcParams['animation.frame_format'] = 'png'

def update_line(num, data, line):
    line.set_data(range(num+1), data[:num])  # Error gets thrown because of this shape mismatch
    return line,
fig1 = plt.figure()

# Fixing random state for reproducibility
np.random.seed(19680801)

size = 25
data = np.random.rand(size)
l, = plt.plot([], [], 'r-')
plt.xlim(0, size)
plt.ylim(0, 1)

line_ani = animation.FuncAnimation(fig1, update_line, size, fargs=(data, l),
                                   interval=50, blit=True)
anim = line_ani.to_jshtml()
plt.close(fig1)

HTML(anim)

Actual outcome

When plotting data with different dimensions while animating we get the following error that is thrown by numpy:

ValueError                                Traceback (most recent call last)
<ipython-input-7-6fb730cf6bdf> in <module>
     22 line_ani = animation.FuncAnimation(fig1, update_line, size, fargs=(data, l),
     23                                    interval=50, blit=True)
---> 24 anim = line_ani.to_jshtml()
     25 plt.close(fig1)
     26 

~/.ml-venv/lib/python3.8/site-packages/matplotlib/animation.py in to_jshtml(self, fps, embed_frames, default_mode)
   1400                                     embed_frames=embed_frames,
   1401                                     default_mode=default_mode)
-> 1402                 self.save(str(path), writer=writer)
   1403                 self._html_representation = path.read_text()
   1404 

~/.ml-venv/lib/python3.8/site-packages/matplotlib/animation.py in save(self, filename, writer, fps, dpi, codec, bitrate, extra_args, metadata, extra_anim, savefig_kwargs, progress_callback)
   1171                 for anim, d in zip(all_anim, data):
   1172                     # TODO: See if turning off blit is really necessary
-> 1173                     anim._draw_next_frame(d, blit=False)
   1174                     if progress_callback is not None:
   1175                         progress_callback(frame_number, total_frames)

~/.ml-venv/lib/python3.8/site-packages/matplotlib/animation.py in _draw_next_frame(self, framedata, blit)
   1207         self._pre_draw(framedata, blit)
   1208         self._draw_frame(framedata)
-> 1209         self._post_draw(framedata, blit)
   1210 
   1211     def _init_draw(self):

~/.ml-venv/lib/python3.8/site-packages/matplotlib/animation.py in _post_draw(self, framedata, blit)
   1232             self._blit_draw(self._drawn_artists)
   1233         else:
-> 1234             self._fig.canvas.draw_idle()
   1235 
   1236     # The rest of the code in this class is to facilitate easy blitting

~/.ml-venv/lib/python3.8/site-packages/matplotlib/backend_bases.py in draw_idle(self, *args, **kwargs)
   2053         if not self._is_idle_drawing:
   2054             with self._idle_draw_cntx():
-> 2055                 self.draw(*args, **kwargs)
   2056 
   2057     def get_width_height(self):

~/.ml-venv/lib/python3.8/site-packages/matplotlib/backends/backend_agg.py in draw(self)
    404              (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
    405               else nullcontext()):
--> 406             self.figure.draw(self.renderer)
    407             # A GUI class may be need to update a window using this draw, so
    408             # don't forget to call the superclass.

~/.ml-venv/lib/python3.8/site-packages/matplotlib/artist.py in draw_wrapper(artist, renderer, *args, **kwargs)
     72     @wraps(draw)
     73     def draw_wrapper(artist, renderer, *args, **kwargs):
---> 74         result = draw(artist, renderer, *args, **kwargs)
     75         if renderer._rasterizing:
     76             renderer.stop_rasterizing()

~/.ml-venv/lib/python3.8/site-packages/matplotlib/artist.py in draw_wrapper(artist, renderer, *args, **kwargs)
     49                 renderer.start_filter()
     50 
---> 51             return draw(artist, renderer, *args, **kwargs)
     52         finally:
     53             if artist.get_agg_filter() is not None:

~/.ml-venv/lib/python3.8/site-packages/matplotlib/figure.py in draw(self, renderer)
   2723 
   2724             self.patch.draw(renderer)
-> 2725             mimage._draw_list_compositing_images(
   2726                 renderer, self, artists, self.suppressComposite)
   2727 

~/.ml-venv/lib/python3.8/site-packages/matplotlib/image.py in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
    130     if not_composite or not has_images:
    131         for a in artists:
--> 132             a.draw(renderer)
    133     else:
    134         # Composite any adjacent images together

~/.ml-venv/lib/python3.8/site-packages/matplotlib/artist.py in draw_wrapper(artist, renderer, *args, **kwargs)
     49                 renderer.start_filter()
     50 
---> 51             return draw(artist, renderer, *args, **kwargs)
     52         finally:
     53             if artist.get_agg_filter() is not None:

~/.ml-venv/lib/python3.8/site-packages/matplotlib/_api/deprecation.py in wrapper(*inner_args, **inner_kwargs)
    429                          else deprecation_addendum,
    430                 **kwargs)
--> 431         return func(*inner_args, **inner_kwargs)
    432 
    433     return wrapper

~/.ml-venv/lib/python3.8/site-packages/matplotlib/axes/_base.py in draw(self, renderer, inframe)
   2923             renderer.stop_rasterizing()
   2924 
-> 2925         mimage._draw_list_compositing_images(renderer, self, artists)
   2926 
   2927         renderer.close_group('axes')

~/.ml-venv/lib/python3.8/site-packages/matplotlib/image.py in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
    130     if not_composite or not has_images:
    131         for a in artists:
--> 132             a.draw(renderer)
    133     else:
    134         # Composite any adjacent images together

~/.ml-venv/lib/python3.8/site-packages/matplotlib/artist.py in draw_wrapper(artist, renderer, *args, **kwargs)
     49                 renderer.start_filter()
     50 
---> 51             return draw(artist, renderer, *args, **kwargs)
     52         finally:
     53             if artist.get_agg_filter() is not None:

~/.ml-venv/lib/python3.8/site-packages/matplotlib/lines.py in draw(self, renderer)
    749 
    750         if self._invalidy or self._invalidx:
--> 751             self.recache()
    752         self.ind_offset = 0  # Needed for contains() method.
    753         if self._subslice and self.axes:

~/.ml-venv/lib/python3.8/site-packages/matplotlib/lines.py in recache(self, always)
    669             y = self._y
    670 
--> 671         self._xy = np.column_stack(np.broadcast_arrays(x, y)).astype(float)
    672         self._x, self._y = self._xy.T  # views
    673 

<__array_function__ internals> in broadcast_arrays(*args, **kwargs)

~/.ml-venv/lib/python3.8/site-packages/numpy/lib/stride_tricks.py in broadcast_arrays(*args, **kwargs)
    262     args = [np.array(_m, copy=False, subok=subok) for _m in args]
    263 
--> 264     shape = _broadcast_shape(*args)
    265 
    266     if all(array.shape == shape for array in args):

~/.ml-venv/lib/python3.8/site-packages/numpy/lib/stride_tricks.py in _broadcast_shape(*args)
    189     # use the old-iterator because np.nditer does not handle size 0 arrays
    190     # consistently
--> 191     b = np.broadcast(*args[:32])
    192     # unfortunately, it cannot handle 32 or more arguments directly
    193     for pos in range(32, len(args), 31):

ValueError: shape mismatch: objects cannot be broadcast to a single shape

Expected outcome

If we try plotting data with inconsistent dimensions (i.e: plt.plot([1,2,3], [1, 2])) we get the following error:

~/.ml-venv/lib/python3.8/site-packages/matplotlib/axes/_base.py in _plot_args(self, tup, kwargs, return_kwargs)
    499 
    500         if x.shape[0] != y.shape[0]:
--> 501             raise ValueError(f"x and y must have same first dimension, but "
    502                              f"have shapes {x.shape} and {y.shape}")
    503         if x.ndim > 2 or y.ndim > 2:

ValueError: x and y must have same first dimension, but have shapes (3,) and (2,)

I would expect the above code to produce a similar error.

Matplotlib version

  • Operating system: Ubuntu 20.04
  • Matplotlib version: 3.3.4+2545.gbd7df7847
  • Matplotlib backend: agg
  • Python version: 3.8.5
  • Jupyter version (if applicable):
    • jupyter core : 4.6.3
    • jupyter-notebook : 6.1.4
    • qtconsole : 5.0.1
    • ipython : 7.18.1
    • ipykernel : 5.3.4
    • jupyter client : 6.1.7
    • jupyter lab : 3.0.9
    • nbconvert : 6.0.7
    • ipywidgets : 7.6.3
    • nbformat : 5.0.7
    • traitlets : 5.0.4
  • Other libraries: N/A
@jklymak
Copy link
Member

jklymak commented Mar 8, 2021

set_data is pretty low level, so it is expected to have weaker catches; further the acceptable arguments for plot are quite varied, so it'll take some care to get this right. But certainly if someone had a reasonable proposal it would be welcome.

@jungerm2
Copy link
Author

jungerm2 commented Mar 8, 2021

Oh, that makes more sense. Although I'm not sure I understand why these checks are done at the high level of plot and not at the lowest possible level (i.e: set_data). Doing these checks at a lower level would ensure they are always done and any errors therefore always caught. Surely there's a performance hit, but probably minimal.

Something like:

if len(x) != len(y):
    raise ValueError(f"x and y must have same dimension, but "
                              f"have shapes {len(x)} and {len(y)}")

on line 652 of the following

def set_data(self, *args):
"""
Set the x and y data.
Parameters
----------
*args : (2, N) array or two 1D arrays
"""
if len(args) == 1:
(x, y), = args
else:
x, y = args
self.set_xdata(x)
self.set_ydata(y)

would surely help, but then this issue still persists if one uses set_xdata/set_ydata. For those, we could check if the incoming data is of that same shape as the old data but this would break if we call set_data with an x/y of equal lengths but different than the old length because set_data uses set_xdata/set_ydata. This could be avoided by passing some sort of no_check=True flag but changing the signature of set_xdata/set_ydata is likely a bad idea.

Any ideas? Are there any other cases I'm missing?

@jklymak
Copy link
Member

jklymak commented Mar 9, 2021

I'm pretty sure that x or y can be 2-D.... So we may need to trace down the logic in plot.

@jungerm2
Copy link
Author

jungerm2 commented Mar 9, 2021

Alright, sure. We can do something like this then:

if x.shape[0] != y.shape[0]:
    raise ValueError(f"x and y must have same first dimension, but "
                     f"have shapes {x.shape} and {y.shape}")
if x.ndim > 2 or y.ndim > 2:
    raise ValueError(f"x and y can be no greater than 2D, but have "
                     f"shapes {x.shape} and {y.shape}")

Still, the problem with set_xdata/set_ydata persists.

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

No branches or pull requests

3 participants