X Tutup
Skip to content
Draft
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
29 changes: 27 additions & 2 deletions lib/matplotlib/backends/backend_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

import matplotlib as mpl
from matplotlib import _api, _text_helpers, _type1font, cbook, dviread
from matplotlib import transforms as mtransforms
from matplotlib._pylab_helpers import Gcf
from matplotlib.backend_bases import (
_Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase,
Expand Down Expand Up @@ -1872,6 +1873,19 @@ def writeMarkers(self):
self.endStream()

def pathCollectionObject(self, gc, path, trans, padding, filled, stroked):
simplify = (
mpl.rcParams.get("path.simplify", False)
and mpl.rcParams.get("path.simplify_threshold", 0) > 0
and getattr(path, "_fill_between_simplify", False)
)

if simplify and trans is not None:
try:
path = path.cleaned(transform=trans, simplify=True)
trans = mtransforms.IdentityTransform()
except Exception:
pass

name = Name('P%d' % len(self.paths))
ob = self.reserveObject('path %d' % len(self.paths))
self.paths.append(
Expand Down Expand Up @@ -1912,12 +1926,16 @@ def pathOperations(path, transform, clip=None, simplify=None, sketch=None):
def writePath(self, path, transform, clip=False, sketch=None):
if clip:
clip = (0.0, 0.0, self.width * 72, self.height * 72)
simplify = path.should_simplify
simplify = (
mpl.rcParams.get("path.simplify", False)
and mpl.rcParams.get("path.simplify_threshold", 0) > 0
and getattr(path, "_fill_between_simplify", False)
)
else:
clip = None
simplify = False
cmds = self.pathOperations(path, transform, clip, simplify=simplify,
sketch=sketch)
sketch=sketch)
self.output(*cmds)

def reserveObject(self, name=''):
Expand Down Expand Up @@ -2095,6 +2113,13 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
should_do_optimization = \
len_path + uses_per_path + 5 < len_path * uses_per_path

any_should_simplify = any(
getattr(path, "_fill_between_simplify", False) for path in paths
)

if any_should_simplify:
should_do_optimization = True

if (not can_do_optimization) or (not should_do_optimization):
return RendererBase.draw_path_collection(
self, gc, master_transform, paths, all_transforms,
Expand Down
22 changes: 18 additions & 4 deletions lib/matplotlib/backends/backend_svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -683,17 +683,23 @@ def _convert_path(self, path, transform=None, clip=None, simplify=None,
clip = (0.0, 0.0, self.width, self.height)
else:
clip = None

if simplify is None:
simplify = (
mpl.rcParams.get("path.simplify", False)
and mpl.rcParams.get("path.simplify_threshold", 0) > 0
and getattr(path, "_fill_between_simplify", False)
)

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
path_data = self._convert_path(
path, trans_and_flip, clip=clip, simplify=simplify,
path, trans_and_flip, clip=clip, simplify=None,
sketch=gc.get_sketch_params())

if gc.get_url() is not None:
Expand Down Expand Up @@ -762,6 +768,13 @@ 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

any_should_simplify = any(
getattr(path, "_fill_between_simplify", False) for path in paths
)
if any_should_simplify:
should_do_optimization = True

if not should_do_optimization:
return super().draw_path_collection(
gc, master_transform, paths, all_transforms,
Expand All @@ -774,8 +787,9 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
writer.start('defs')
for i, (path, transform) in enumerate(self._iter_collection_raw_paths(
master_transform, paths, all_transforms)):

transform = Affine2D(transform.get_matrix()).scale(1.0, -1.0)
d = self._convert_path(path, transform, simplify=False)
d = self._convert_path(path, transform, simplify=None)
oid = 'C{:x}_{:x}_{}'.format(
self._path_collection_id, i, self._make_id('', d))
writer.element('path', id=oid, d=d)
Expand Down
6 changes: 6 additions & 0 deletions lib/matplotlib/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -1494,6 +1494,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
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
X Tutup