Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions lib/matplotlib/backends/backend_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -1810,13 +1810,15 @@ def pathOperations(path, transform, clip=None, simplify=None, sketch=None):
Op.closepath.value],
True))]

def writePath(self, path, transform, clip=False, sketch=None):
def writePath(self, path, transform, clip=False, sketch=None, simplify=None):
if clip:
clip = (0.0, 0.0, self.width * 72, self.height * 72)
simplify = path.should_simplify
if simplify is None:
simplify = path.should_simplify
else:
clip = None
simplify = False
if simplify is None:
simplify = False
cmds = self.pathOperations(path, transform, clip, simplify=simplify,
sketch=sketch)
self.output(*cmds)
Expand Down Expand Up @@ -1947,10 +1949,15 @@ def draw_image(self, gc, x, y, im, transform=None):
def draw_path(self, gc, path, transform, rgbFace=None):
# docstring inherited
self.check_gc(gc, rgbFace)
no_hatch = gc.get_hatch_path() is None
clip = rgbFace is None and no_hatch
simplify = None
if (not clip and no_hatch
and getattr(path, "_fill_between_simplify", False)):
simplify = (mpl.rcParams['path.simplify']
and mpl.rcParams['path.simplify_threshold'] > 0)
self.file.writePath(
path, transform,
rgbFace is None and gc.get_hatch_path() is None,
gc.get_sketch_params())
path, transform, clip, gc.get_sketch_params(), simplify=simplify)
self.file.output(self.gc.paint())

def draw_path_collection(self, gc, master_transform, paths, all_transforms,
Expand Down
12 changes: 10 additions & 2 deletions lib/matplotlib/backends/backend_svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -683,15 +683,22 @@ def _convert_path(self, path, transform=None, clip=None, simplify=None,
clip = (0.0, 0.0, self.width, self.height)
else:
clip = None

return _path.convert_to_string(
path, transform, clip, simplify, sketch, 6,
[b'M', b'L', b'Q', b'C', b'z'], False).decode('ascii')

def draw_path(self, gc, path, transform, rgbFace=None):
# docstring inherited
trans_and_flip = self._make_flip_transform(transform)
clip = (rgbFace is None and gc.get_hatch_path() is None)
simplify = path.should_simplify and clip
no_hatch = gc.get_hatch_path() is None
clip = rgbFace is None and no_hatch
fill_between = getattr(path, "_fill_between_simplify", False) and no_hatch
if fill_between:
simplify = (mpl.rcParams['path.simplify']
and mpl.rcParams['path.simplify_threshold'] > 0)
else:
simplify = path.should_simplify and clip
path_data = self._convert_path(
path, trans_and_flip, clip=clip, simplify=simplify,
sketch=gc.get_sketch_params())
Expand Down Expand Up @@ -762,6 +769,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
paths, all_transforms, offsets, facecolors, edgecolors)
should_do_optimization = \
len_path + 9 * uses_per_path + 3 < (len_path + 5) * uses_per_path

if not should_do_optimization:
return super().draw_path_collection(
gc, master_transform, paths, all_transforms,
Expand Down
6 changes: 6 additions & 0 deletions lib/matplotlib/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -1502,6 +1502,12 @@ def set_data(self, t, f1, f2, *, where=None):
verts = self._make_verts(t, f1, f2, where)
self.set_verts(verts)

def set_verts(self, verts, closed=True):
super().set_verts(verts, closed=closed)
for path in self._paths:
path._fill_between_simplify = True
set_paths = set_verts

def get_datalim(self, transData):
"""Calculate the data limits and return them as a `.Bbox`."""
datalim = transforms.Bbox.null()
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/tests/test_agg_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@


@image_comparison(baseline_images=['agg_filter_alpha'],
extensions=['gif', 'png', 'pdf'], style='mpl20')
extensions=['png', 'pdf'], style='mpl20')
def test_agg_filter_alpha():
ax = plt.axes()
x, y = np.mgrid[0:7, 0:8]
Expand Down
3 changes: 2 additions & 1 deletion lib/matplotlib/tests/test_backend_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ def test_indexed_image():

with pikepdf.Pdf.open(buf) as pdf:
page, = pdf.pages
image, = page.images.values()
images = page.get_images() if hasattr(page, 'get_images') else page.images
image, = images.values()
pdf_image = pikepdf.PdfImage(image)
assert pdf_image.indexed
pil_image = pdf_image.as_pil_image()
Expand Down
154 changes: 154 additions & 0 deletions lib/matplotlib/tests/test_fill_between_simplify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
from dataclasses import dataclass

import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import pytest


@dataclass(frozen=True)
class FillScenario:
name: str
y1: np.ndarray
y2: np.ndarray | float
where: np.ndarray | None = None
interpolate: bool = False


def _make_scenarios(n=2000, seed=4242):
t = np.linspace(0, 10, n)

f1 = np.sin(t)
f2 = 0.2 * np.cos(2 * t)

rng = np.random.default_rng(seed)
where_random = rng.random(n) > 0.3

c1 = np.sin(t)
c2 = 0.9 * np.cos(t)

scenarios = {
"where_random": FillScenario(
name="where_random",
y1=f1,
y2=f2,
where=where_random,
),
"interpolate_cross": FillScenario(
name="interpolate_cross",
y1=c1,
y2=c2,
where=c1 > c2,
interpolate=True,
),
}

f3 = np.sin(t) + 0.5
f4 = -0.5 * np.ones_like(t)

where_even = np.zeros_like(t, dtype=bool)
where_even[::2] = True

where_blocks = np.zeros_like(t, dtype=bool)
where_blocks[n // 10: 2 * n // 10] = True
where_blocks[5 * n // 10: 7 * n // 10] = True

multi_inputs = {
"t": t,
"f1": f1,
"f2": f2,
"f3": f3,
"f4": f4,
"where_even": where_even,
"where_blocks": where_blocks,
"where_random": where_random,
}

return t, scenarios, multi_inputs


def _save_single_fill(path, t, scenario, threshold):
with mpl.rc_context({
"path.simplify": True,
"path.simplify_threshold": threshold,
}):
fig, ax = plt.subplots()
ax.fill_between(
t,
scenario.y1,
scenario.y2,
where=scenario.where,
interpolate=scenario.interpolate,
alpha=0.5,
)
ax.set_xlim(t[0], t[-1])
fig.savefig(path)
plt.close(fig)


def _save_multi_fill(path, multi_inputs, threshold):
t = multi_inputs["t"]

with mpl.rc_context({
"path.simplify": True,
"path.simplify_threshold": threshold,
}):
fig, ax = plt.subplots()
ax.fill_between(
t, multi_inputs["f1"], multi_inputs["f2"],
where=multi_inputs["where_blocks"], alpha=0.5,
)
ax.fill_between(
t, multi_inputs["f3"], multi_inputs["f4"],
where=multi_inputs["where_random"], alpha=0.5,
)
ax.fill_between(
t, multi_inputs["f2"], multi_inputs["f4"],
where=multi_inputs["where_even"], alpha=0.5,
)
ax.set_xlim(t[0], t[-1])
fig.savefig(path)
plt.close(fig)


def _assert_smaller(size0, size1, label):
assert size1 < size0, (
f"{label}: expected threshold=1.0 output to be smaller than "
f"threshold=0.0, got {size0} -> {size1}"
)


@pytest.mark.parametrize("ext", ["svg", "pdf"])
@pytest.mark.parametrize("scenario_key", ["where_random", "interpolate_cross"])
def test_fill_between_simplify_reduces_output_size(tmp_path, ext, scenario_key):
t, scenarios, _ = _make_scenarios()
scenario = scenarios[scenario_key]

path0 = tmp_path / f"{scenario.name}_thr0.{ext}"
path1 = tmp_path / f"{scenario.name}_thr1.{ext}"

_save_single_fill(path0, t, scenario, threshold=0.0)
_save_single_fill(path1, t, scenario, threshold=1.0)

_assert_smaller(
path0.stat().st_size,
path1.stat().st_size,
f"{scenario.name} {ext}",
)


@pytest.mark.parametrize("ext", ["svg", "pdf"])
def test_fill_between_multi_regions_simplify_reduces_output_size(tmp_path, ext):
_, _, multi_inputs = _make_scenarios()

path0 = tmp_path / f"multi_regions_thr0.{ext}"
path1 = tmp_path / f"multi_regions_thr1.{ext}"

_save_multi_fill(path0, multi_inputs, threshold=0.0)
_save_multi_fill(path1, multi_inputs, threshold=1.0)

_assert_smaller(
path0.stat().st_size,
path1.stat().st_size,
f"multi_regions {ext}",
)
Loading