matplotlib/mplcairo

Unexpected fallback to Qt5Agg and exception with some zooming conditions

afvincent opened this issue ยท 6 comments

Disclaimer

I am neither sure if there is actually 1 or 2 issues, nor that it is really an issue with mplcairo or something more subtle but definitively part of the vanilla Matplotlib backends ๐Ÿ‘ .

Code snippet

# -*- coding: utf-8 -*-

"""
Filename: example_issue_mplcairo.py
"""

import numpy as np
import matplotlib
matplotlib.use("module://mplcairo.qt")  # my default, but just to be sure
#matplotlib.use("Qt5Agg")  # no error is triggered with this backend
print("#0 " + matplotlib.get_backend())
import matplotlib.pyplot as plt

# Prepare data
# ############

# NB: using "real" data bc I couldn't trigger the issue with artificial ones.

data = np.array([
    [0.9010783810870, 0.01630277666922, 0.9094683896745, 0.01026045742829],
    [0.9570786503693, 0.16706294351760, 0.9450353328361, 0.05505800869503],
    [0.9571383849633, 0.16753128736241, 0.9528604472226, 0.08115013837996],
    [0.9571854383087, 0.16790131216395, 0.9560082084967, 0.09529819473078],
    [0.9572052469505, 0.16805737890754, 0.9571888177871, 0.10130912344386],
    [0.9572556034709, 0.16845490694844, 0.9595591931432, 0.11474075575411],
    [0.9572566034023, 0.16846281207996, 0.9636070458793, 0.14280470855909],
    [0.9572595678960, 0.16848625101237, 0.9636078558762, 0.14281109000281],
    [0.9572636787008, 0.16851875978305, 0.9636083939103, 0.14281532901640],
    ])

raw_x0 = data[:, 0]
raw_y0 = data[:, 1]
raw_x1 = data[:, 2]
raw_y1 = data[:, 3]

vmin = max(raw_x0.min(), raw_x1.min())
vmax = min(raw_x0.max(), raw_x1.max())
xx = np.concatenate((raw_x0[(raw_x0 >= vmin) & (raw_x0 <= vmax)],
                     raw_x1[(raw_x1 >= vmin) & (raw_x1 <= vmax)]))
xx.sort()

y0 = np.interp(xx, raw_x0, raw_y0)
y1 = np.interp(xx, raw_x1, raw_y1)

# Prepare figure
# ##############

# Weirdly, the return value from `matplotlib.get_backend()` changes after
# instantiating the figure.

print("#1 " + matplotlib.get_backend())
fig, ax = plt.subplots(num="example_anntzer", clear=True)
print("#2 " + matplotlib.get_backend())

# Plot
# ####

ax.plot(raw_x0, raw_y0, marker="v", color="tab:red", mfc="white")
ax.plot(raw_x1, raw_y1, marker="^", color="tab:blue", mfc="white")
ax.fill_between(xx, y0, y1, interpolate=True, color="tab:purple", alpha=0.5)

fig.tight_layout()
print("#3 " + matplotlib.get_backend())
fig.show()

# Zooming on the x-values corresponding to the last group of values for x0
# will trigger an error with 'mplcairo', but not with 'Qt5Agg'.

Actual outcome

First, why would matplotlib.get_backend() stop returning 'module://mplcairo.qt'? See

In [1]: run example_issue_mplcairo.py
#0 module://mplcairo.qt
#1 module://mplcairo.qt
#2 Qt5Agg
#3 Qt5Agg

Besides, when zooming like on the following screenshot
zooming_triggers_the_exception
I get an error with the following traceback

In [2]: Traceback (most recent call last):
  File "/home/adrien/anaconda3/lib/python3.6/site-packages/matplotlib/backends/backend_qt5.py", line 519, in _draw_idle
    self.draw()
  File "/home/adrien/anaconda3/lib/python3.6/site-packages/mplcairo/base.py", line 225, in draw
    self.figure.draw(self.get_renderer())
  File "/home/adrien/anaconda3/lib/python3.6/site-packages/matplotlib/artist.py", line 55, in draw_wrapper
    return draw(artist, renderer, *args, **kwargs)
  File "/home/adrien/anaconda3/lib/python3.6/site-packages/matplotlib/figure.py", line 1475, in draw
    renderer, self, artists, self.suppressComposite)
  File "/home/adrien/anaconda3/lib/python3.6/site-packages/matplotlib/image.py", line 141, in _draw_list_compositing_images
    a.draw(renderer)
  File "/home/adrien/anaconda3/lib/python3.6/site-packages/matplotlib/artist.py", line 55, in draw_wrapper
    return draw(artist, renderer, *args, **kwargs)
  File "/home/adrien/anaconda3/lib/python3.6/site-packages/matplotlib/axes/_base.py", line 2607, in draw
    mimage._draw_list_compositing_images(renderer, self, artists)
  File "/home/adrien/anaconda3/lib/python3.6/site-packages/matplotlib/image.py", line 141, in _draw_list_compositing_images
    a.draw(renderer)
  File "/home/adrien/anaconda3/lib/python3.6/site-packages/matplotlib/artist.py", line 55, in draw_wrapper
    return draw(artist, renderer, *args, **kwargs)
  File "/home/adrien/anaconda3/lib/python3.6/site-packages/matplotlib/collections.py", line 911, in draw
    Collection.draw(self, renderer)
  File "/home/adrien/anaconda3/lib/python3.6/site-packages/matplotlib/artist.py", line 55, in draw_wrapper
    return draw(artist, renderer, *args, **kwargs)
  File "/home/adrien/anaconda3/lib/python3.6/site-packages/matplotlib/collections.py", line 337, in draw
    self._offset_position)
ValueError: invalid value (typically too big) for the size of the input (surface, pattern, etc.)

Expected outcome

In [1]: run example_issue_mplcairo.py
#0 module://mplcairo.qt
#1 module://mplcairo.qt
#2 module://mplcairo.qt
#3 module://mplcairo.qt

and no exception raised when zooming on the plot :/.

Platform

  • Python: 3.6 from conda
  • Matplotlib: 2.2.2 from conda
  • mplcairo: from a nighty build made by @anntzer when I was struggling with installing mplcairo...

What's mplcairo.__version__? (That'll give me the commit the nightly is from.)

I see where the exception is coming from. Do you, by any chance, have the path.simplify_threshold set to a nonzero value? Does setting it to zero fix the issue?

If it does, what's happening is the following:
In order to improve the performance of drawing scatterplots with various sizes and colors, whenever it is asked to draw a PathCollection (the class behind scatterplots), mplcairo keeps a cache of the scatter elements drawn at various sizes (lazily populated on-demand), so that rasterization is performed only once and later uses can just "stamp" the image from the buffer.
Unfortunately, fill_between is also implemented using PathCollection, so mplcairo tries to do the same here... and ends up trying to draw the fill_between polygon at an enourmous magnification (at that point it doesn't realize yet that the thing will be cropped).

I plan to fix that by following the same strategy as for draw_markers, which is to fall back on "naive" drawing when the polygon is bigger than the canvas size (for example).

I cannot reproduce the backend changing issue (I get module://mplcairo.qt all the time). What's your version of ipython, do you have anything of interest in your ipython_config.py, do you use e.g. %matplotlib auto, do you use MPLBACKEND, etc.?

FWIW (I still have to read #4).

The mplcairo version that I am using:

In [1]: mplcairo.__version__
Out[1]: '0.1a1.post15+g322e722'

Excerpt from my matplotlibrc file:

### SAVING FIGURES
#path.simplify : True   # When True, simplify paths by removing "invisible"
                        # points to reduce file size and increase rendering
                        # speed
#path.simplify_threshold : 0.1  # The threshold of similarity below which
                                # vertices will be removed in the simplification
                                # process
#path.snap : True # When True, rectilinear axis-aligned paths will be snapped to
                  # the nearest pixel when certain criteria are met.  When False,
                  # paths will never be snapped.
#path.sketch : None # May be none, or a 3-tuple of the form (scale, length,
                    # randomness).
                    # *scale* is the amplitude of the wiggle
                    # perpendicular to the line (in pixels).  *length*
                    # is the length of the wiggle along the line (in
                    # pixels).  *randomness* is the factor by which
                    # the length is randomly scaled.

Excerpt from my .bashrc file:

export MPLBACKEND="module://mplcairo.qt"

I am using IPython 6.2.1 (from conda). AFAICT, I am not using a peculiar ipython_config.py file (there is no such file in $HOME/.ipython/), but indeed it looks like an IPython issue:

python example_issue_mplcairo.py 
#0 module://mplcairo.qt
#1 module://mplcairo.qt
#2 module://mplcairo.qt
#3 module://mplcairo.qt

OK, I can reproduce it now. What's your ~/.config/matplotlib/matplotlibrc?

As it is several hundreds of line long, see the gist here. (I cannot remember if I modified it a lot or not compared to the default version.)

FWIW, (at least on my side) it looks like #5 fixes the second half of the issue ;).