Skip to content

Force clipped-log when drawing log-histograms. #9305

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
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
32 changes: 32 additions & 0 deletions lib/matplotlib/artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import six

from collections import OrderedDict, namedtuple
import contextlib
from functools import wraps
import inspect
import re
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions lib/matplotlib/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion lib/matplotlib/patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
18 changes: 18 additions & 0 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()