From 957a5a15069d341c98cd8598f9b1e3d2cfb07326 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sat, 7 Oct 2017 02:00:27 -0700 Subject: [PATCH] Force clipped-log for hist/errorbar/fill_between. --- lib/matplotlib/artist.py | 32 +++++++++++++++++++++++++++++++ lib/matplotlib/axes/_axes.py | 11 +++++++++-- lib/matplotlib/collections.py | 5 +++-- lib/matplotlib/patches.py | 5 ++++- lib/matplotlib/tests/test_axes.py | 18 +++++++++++++++++ 5 files changed, 66 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 21edc744c7ad..ecb3b90a4b2b 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -4,6 +4,7 @@ import six from collections import OrderedDict, namedtuple +import contextlib from functools import wraps import inspect import re @@ -120,6 +121,22 @@ def __init__(self): self._path_effects = rcParams['path.effects'] self._sticky_edges = _XYPair([], []) + # When plotting in log-scale, force the use of clip mode instead of + # mask. The typical (internal) use case is log-scaled bar plots and + # error bars. Ideally we'd want BarContainers / ErrorbarContainers + # to have their own show() method which takes care of the patching, + # but right now Containers are not taken into account during + # the draw; instead their components (in the case of bar plots, + # these are Rectangle patches; in the case of ErrorbarContainers, + # LineCollections) are drawn individually, so tracking the force_clip + # state must be done by the component artists. Note that handling of + # _force_clip_in_log_scale must be done by the individual artists' + # draw implementation; right now only Patches and Collections support + # it. The `_forcing_clip_in_log_scale` decorator may be helpful to + # implement such support, it should typically be applied around a call + # to `transform_path_non_affine`. + self._force_clip_in_log_scale = False + def __getstate__(self): d = self.__dict__.copy() # remove the unpicklable remove method, this will get re-added on load @@ -779,6 +796,21 @@ def draw(self, renderer, *args, **kwargs): return self.stale = False + @contextlib.contextmanager + def _forcing_clip_in_log_scale(self): + # See _force_clip_in_log_scale for explanation. + fvs = {} + if self._force_clip_in_log_scale and self.axes: + for axis in self.axes._get_axis_list(): + if axis.get_scale() == "log": + fvs[axis] = axis._scale._transform._fill_value + axis._scale._transform._fill_value = 1e-300 + try: + yield + finally: + for axis, fv in fvs.items(): + axis._scale._transform._fill_value = fv + def set_alpha(self, alpha): """ Set the alpha value used for blending - not supported on diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 2275361aa17c..2dceb8990e1e 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2158,6 +2158,7 @@ def bar(self, *args, **kwargs): r.sticky_edges.y.append(b) elif orientation == 'horizontal': r.sticky_edges.x.append(l) + r._force_clip_in_log_scale = True self.add_patch(r) patches.append(r) @@ -3053,7 +3054,9 @@ def extract_err(err, data): if xlolims.any(): yo, _ = xywhere(y, right, xlolims & everymask) lo, ro = xywhere(x, right, xlolims & everymask) - barcols.append(self.hlines(yo, lo, ro, **eb_lines_style)) + ebs = self.hlines(yo, lo, ro, **eb_lines_style) + ebs._force_clip_in_log_scale = True + barcols.append(ebs) rightup, yup = xywhere(right, y, xlolims & everymask) if self.xaxis_inverted(): marker = mlines.CARETLEFTBASE @@ -3092,7 +3095,9 @@ def extract_err(err, data): if noylims.any(): xo, _ = xywhere(x, lower, noylims & everymask) lo, uo = xywhere(lower, upper, noylims & everymask) - barcols.append(self.vlines(xo, lo, uo, **eb_lines_style)) + ebs = self.vlines(xo, lo, uo, **eb_lines_style) + ebs._force_clip_in_log_scale = True + barcols.append(ebs) if capsize > 0: caplines.append(mlines.Line2D(xo, lo, marker='_', **eb_cap_style)) @@ -4905,6 +4910,7 @@ def get_interp_point(ind): polys.append(X) collection = mcoll.PolyCollection(polys, **kwargs) + collection._force_clip_in_log_scale = True # now update the datalim and autoscale XY1 = np.array([x[where], y1[where]]).T @@ -5057,6 +5063,7 @@ def get_interp_point(ind): polys.append(Y) collection = mcoll.PolyCollection(polys, **kwargs) + collection._force_clip_in_log_scale = True # now update the datalim and autoscale X1Y = np.array([x1[where], y[where]]).T diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index ad660e97ecbd..29ce54027212 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -232,8 +232,9 @@ def _prepare_points(self): offsets = np.column_stack([xs, ys]) if not transform.is_affine: - paths = [transform.transform_path_non_affine(path) - for path in paths] + with self._forcing_clip_in_log_scale(): + paths = [transform.transform_path_non_affine(path) + for path in paths] transform = transform.get_affine() if not transOffset.is_affine: offsets = transOffset.transform_non_affine(offsets) diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 96a9b8bd1ddb..f2f2c20bab16 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -564,7 +564,10 @@ def draw(self, renderer): path = self.get_path() transform = self.get_transform() - tpath = transform.transform_path_non_affine(path) + + with self._forcing_clip_in_log_scale(): + tpath = transform.transform_path_non_affine(path) + affine = transform.get_affine() if self.get_path_effects(): diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 70a7d5ed3891..d0f482c8ce6f 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -5412,3 +5412,21 @@ def test_patch_deprecations(): assert fig.patch == fig.figurePatch assert len(w) == 2 + + +@pytest.mark.parametrize("plotter", + [lambda ax: ax.bar([0, 1], [1, 2]), + lambda ax: ax.errorbar([0, 1], [2, 2], [1, 3]), + lambda ax: ax.fill_between([0, 1], [1, 2], [1, 0])]) +def test_clipped_log_zero(plotter): + fig, ax = plt.subplots() + plotter(ax) + ax.set_yscale("log") + png1 = io.BytesIO() + fig.savefig(png1, format="png") + fig, ax = plt.subplots() + plotter(ax) + ax.set_yscale("log", nonposy="clip") + png2 = io.BytesIO() + fig.savefig(png2, format="png") + assert png1.getvalue() == png2.getvalue()