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() pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy