diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index b937c64fce95..4a62f5a921cd 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -148,18 +148,65 @@ def draw_path(self, gc, path, transform, rgbFace=None): c = c[ii0:ii1] c[0] = Path.MOVETO # move to end of last chunk p = Path(v, c) + p.simplify_threshold = path.simplify_threshold try: self._renderer.draw_path(gc, p, transform, rgbFace) except OverflowError as err: - raise OverflowError( - "Exceeded cell block limit (set 'agg.path.chunksize' " - "rcparam)") from err + msg = ( + "Exceeded cell block limit in Agg.\n\n" + "Please reduce the value of " + f"rcParams['agg.path.chunksize'] (currently {nmax}) " + "or increase the path simplification threshold" + "(rcParams['path.simplify_threshold'] = " + f"{mpl.rcParams['path.simplify_threshold']:.2f} by " + "default and path.simplify_threshold = " + f"{path.simplify_threshold:.2f} on the input)." + ) + raise OverflowError(msg) from None else: try: self._renderer.draw_path(gc, path, transform, rgbFace) except OverflowError as err: - raise OverflowError("Exceeded cell block limit (set " - "'agg.path.chunksize' rcparam)") from err + cant_chunk = '' + if rgbFace is not None: + cant_chunk += "- can not split filled path\n" + if gc.get_hatch() is not None: + cant_chunk += "- can not split hatched path\n" + if not path.should_simplify: + cant_chunk += "- path.should_simplify is False\n" + if len(cant_chunk): + msg = ( + "Exceeded cell block limit in Agg, however for the " + "following reasons:\n\n" + f"{cant_chunk}\n" + "we can not automatically split up this path to draw." + "\n\nPlease manually simplify your path." + ) + + else: + inc_threshold = ( + "or increase the path simplification threshold" + "(rcParams['path.simplify_threshold'] = " + f"{mpl.rcParams['path.simplify_threshold']} " + "by default and path.simplify_threshold " + f"= {path.simplify_threshold} " + "on the input)." + ) + if nmax > 100: + msg = ( + "Exceeded cell block limit in Agg. Please reduce " + "the value of rcParams['agg.path.chunksize'] " + f"(currently {nmax}) {inc_threshold}" + ) + else: + msg = ( + "Exceeded cell block limit in Agg. Please set " + "the value of rcParams['agg.path.chunksize'], " + f"(currently {nmax}) to be greater than 100 " + + inc_threshold + ) + + raise OverflowError(msg) from None def draw_mathtext(self, gc, x, y, s, prop, angle): """Draw mathtext using :mod:`matplotlib.mathtext`.""" diff --git a/lib/matplotlib/tests/test_agg.py b/lib/matplotlib/tests/test_agg.py index 14573f5941f6..0e4abf86fe02 100644 --- a/lib/matplotlib/tests/test_agg.py +++ b/lib/matplotlib/tests/test_agg.py @@ -8,9 +8,12 @@ from matplotlib import ( collections, path, pyplot as plt, transforms as mtransforms, rcParams) -from matplotlib.image import imread +from matplotlib.backends.backend_agg import RendererAgg from matplotlib.figure import Figure +from matplotlib.image import imread +from matplotlib.path import Path from matplotlib.testing.decorators import image_comparison +from matplotlib.transforms import IdentityTransform def test_repeated_save_with_alpha(): @@ -72,10 +75,10 @@ def test_marker_with_nan(): def test_long_path(): buff = io.BytesIO() - - fig, ax = plt.subplots() - np.random.seed(0) - points = np.random.rand(70000) + fig = Figure() + ax = fig.subplots() + points = np.ones(100_000) + points[::2] *= -1 ax.plot(points) fig.savefig(buff, format='png') @@ -251,3 +254,79 @@ def test_draw_path_collection_error_handling(): ax.scatter([1], [1]).set_paths(path.Path([(0, 1), (2, 3)])) with pytest.raises(TypeError): fig.canvas.draw() + + +@pytest.fixture +def chunk_limit_setup(): + N = 100_000 + dpi = 500 + w = 5*dpi + h = 6*dpi + + # just fit in the width + x = np.linspace(0, w, N) + # and go top-to-bottom + y = np.ones(N) * h + y[::2] = 0 + + idt = IdentityTransform() + # make a renderer + ra = RendererAgg(w, h, dpi) + # setup the minimal gc to draw a line + gc = ra.new_gc() + gc.set_linewidth(1) + gc.set_foreground('r') + # make a Path + p = Path(np.vstack((x, y)).T) + # effectively disable path simplification (but leaving it "on") + p.simplify_threshold = 0 + + return ra, gc, p, idt + + +def test_chunksize_hatch_fail(chunk_limit_setup): + ra, gc, p, idt = chunk_limit_setup + + gc.set_hatch('/') + + with pytest.raises(OverflowError, match='hatched path'): + ra.draw_path(gc, p, idt) + + +def test_chunksize_rgbFace_fail(chunk_limit_setup): + ra, gc, p, idt = chunk_limit_setup + + with pytest.raises(OverflowError, match='filled path'): + ra.draw_path(gc, p, idt, (1, 0, 0)) + + +def test_chunksize_no_simplify_fail(chunk_limit_setup): + ra, gc, p, idt = chunk_limit_setup + p.should_simplify = False + with pytest.raises(OverflowError, match="should_simplify is False"): + ra.draw_path(gc, p, idt) + + +def test_chunksize_zero(chunk_limit_setup): + ra, gc, p, idt = chunk_limit_setup + # set to zero to disable, currently defaults to 0, but lets be sure + rcParams['agg.path.chunksize'] = 0 + with pytest.raises(OverflowError, match='Please set'): + ra.draw_path(gc, p, idt) + + +def test_chunksize_too_big_to_chunk(chunk_limit_setup): + ra, gc, p, idt = chunk_limit_setup + # set big enough that we do not try to chunk + rcParams['agg.path.chunksize'] = 1_000_000 + with pytest.raises(OverflowError, match='Please reduce'): + ra.draw_path(gc, p, idt) + + +def test_chunksize_toobig_chunks(chunk_limit_setup): + ra, gc, p, idt = chunk_limit_setup + # small enough we will try to chunk, but big enough we will fail + # to render + rcParams['agg.path.chunksize'] = 90_000 + with pytest.raises(OverflowError, match='Please reduce'): + ra.draw_path(gc, p, idt) diff --git a/lib/matplotlib/tests/test_simplification.py b/lib/matplotlib/tests/test_simplification.py index 952e890ce660..0749d0f3a115 100644 --- a/lib/matplotlib/tests/test_simplification.py +++ b/lib/matplotlib/tests/test_simplification.py @@ -305,8 +305,8 @@ def test_start_with_moveto(): def test_throw_rendering_complexity_exceeded(): plt.rcParams['path.simplify'] = False - xx = np.arange(200000) - yy = np.random.rand(200000) + xx = np.arange(2_000_000) + yy = np.random.rand(2_000_000) yy[1000] = np.nan fig, ax = plt.subplots() diff --git a/src/_backend_agg.cpp b/src/_backend_agg.cpp index 0a9e7ab7e160..79575697a08b 100644 --- a/src/_backend_agg.cpp +++ b/src/_backend_agg.cpp @@ -45,7 +45,7 @@ RendererAgg::RendererAgg(unsigned int width, unsigned int height, double dpi) rendererBase(), rendererAA(), rendererBin(), - theRasterizer(8192), + theRasterizer(32768), lastclippath(NULL), _fill_color(agg::rgba(1, 1, 1, 0)) {