Skip to content

Commit 668ff58

Browse files
tacaswellMeeseeksDev[bot]
authored andcommitted
Backport PR #9477: In LogTransform, clip after log, not before.
1 parent 0144734 commit 668ff58

File tree

11 files changed

+86
-37
lines changed

11 files changed

+86
-37
lines changed

doc/api/api_changes.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,24 @@ out what caused the breakage and how to fix it by updating your code.
1010
For new features that were added to Matplotlib, please see
1111
:ref:`whats-new`.
1212

13+
API Changes in 2.1.1
14+
====================
15+
16+
Default behavior of log scales reverted to clip <= 0 values
17+
-----------------------------------------------------------
18+
19+
The change it 2.1.0 to mask in logscale by default had more disruptive
20+
changes than anticipated and has been reverted, however the clipping is now
21+
done in a way that fixes the issues that motivated changing the default behavior
22+
to ``'mask'``.
23+
24+
As a side effect of this change, error bars which go negative now work as expected
25+
on log scales.
26+
1327
API Changes in 2.1.0
1428
====================
1529

30+
1631
Default behavior of log scales changed to mask <= 0 values
1732
----------------------------------------------------------
1833

lib/matplotlib/axes/_axes.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1556,11 +1556,9 @@ def loglog(self, *args, **kwargs):
15561556

15571557
dx = {'basex': kwargs.pop('basex', 10),
15581558
'subsx': kwargs.pop('subsx', None),
1559-
'nonposx': kwargs.pop('nonposx', 'mask'),
15601559
}
15611560
dy = {'basey': kwargs.pop('basey', 10),
15621561
'subsy': kwargs.pop('subsy', None),
1563-
'nonposy': kwargs.pop('nonposy', 'mask'),
15641562
}
15651563

15661564
self.set_xscale('log', **dx)
@@ -2851,11 +2849,6 @@ def errorbar(self, x, y, yerr=None, xerr=None,
28512849
Valid kwargs for the marker properties are
28522850
28532851
%(Line2D)s
2854-
2855-
Notes
2856-
-----
2857-
Error bars with negative values will not be shown when plotted on a
2858-
logarithmic axis.
28592852
"""
28602853
kwargs = cbook.normalize_kwargs(kwargs, _alias_map)
28612854
# anything that comes in as 'None', drop so the default thing

lib/matplotlib/axes/_base.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2970,11 +2970,6 @@ def set_xscale(self, value, **kwargs):
29702970
29712971
matplotlib.scale.LogisticTransform : logit transform
29722972
"""
2973-
# If the scale is being set to log, mask nonposx to prevent headaches
2974-
# around zero
2975-
if value.lower() == 'log' and 'nonposx' not in kwargs:
2976-
kwargs['nonposx'] = 'mask'
2977-
29782973
g = self.get_shared_x_axes()
29792974
for ax in g.get_siblings(self):
29802975
ax.xaxis._set_scale(value, **kwargs)
@@ -3292,11 +3287,6 @@ def set_yscale(self, value, **kwargs):
32923287
32933288
matplotlib.scale.LogisticTransform : logit transform
32943289
"""
3295-
# If the scale is being set to log, mask nonposy to prevent headaches
3296-
# around zero
3297-
if value.lower() == 'log' and 'nonposy' not in kwargs:
3298-
kwargs['nonposy'] = 'mask'
3299-
33003290
g = self.get_shared_y_axes()
33013291
for ax in g.get_siblings(self):
33023292
ax.yaxis._set_scale(value, **kwargs)

lib/matplotlib/scale.py

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,24 @@ class LogTransformBase(Transform):
9292

9393
def __init__(self, nonpos):
9494
Transform.__init__(self)
95-
if nonpos == 'mask':
96-
self._fill_value = np.nan
97-
else:
98-
self._fill_value = 1e-300
95+
self._clip = {"clip": True, "mask": False}[nonpos]
9996

10097
def transform_non_affine(self, a):
101-
with np.errstate(invalid="ignore"):
102-
a = np.where(a <= 0, self._fill_value, a)
103-
return np.divide(np.log(a, out=a), np.log(self.base), out=a)
98+
with np.errstate(divide="ignore", invalid="ignore"):
99+
out = np.log(a)
100+
out /= np.log(self.base)
101+
if self._clip:
102+
# SVG spec says that conforming viewers must support values up
103+
# to 3.4e38 (C float); however experiments suggest that Inkscape
104+
# (which uses cairo for rendering) runs into cairo's 24-bit limit
105+
# (which is apparently shared by Agg).
106+
# Ghostscript (used for pdf rendering appears to overflow even
107+
# earlier, with the max value around 2 ** 15 for the tests to pass.
108+
# On the other hand, in practice, we want to clip beyond
109+
# np.log10(np.nextafter(0, 1)) ~ -323
110+
# so 1000 seems safe.
111+
out[a <= 0] = -1000
112+
return out
104113

105114

106115
class InvertedLogTransformBase(Transform):
@@ -220,11 +229,17 @@ def __init__(self, axis, **kwargs):
220229
if axis.axis_name == 'x':
221230
base = kwargs.pop('basex', 10.0)
222231
subs = kwargs.pop('subsx', None)
223-
nonpos = kwargs.pop('nonposx', 'mask')
232+
nonpos = kwargs.pop('nonposx', 'clip')
224233
else:
225234
base = kwargs.pop('basey', 10.0)
226235
subs = kwargs.pop('subsy', None)
227-
nonpos = kwargs.pop('nonposy', 'mask')
236+
nonpos = kwargs.pop('nonposy', 'clip')
237+
238+
if len(kwargs):
239+
raise ValueError(("provided too many kwargs, can only pass "
240+
"{'basex', 'subsx', nonposx'} or "
241+
"{'basey', 'subsy', nonposy'}. You passed ") +
242+
"{!r}".format(kwargs))
228243

229244
if nonpos not in ['mask', 'clip']:
230245
raise ValueError("nonposx, nonposy kwarg must be 'mask' or 'clip'")
@@ -432,18 +447,17 @@ class LogitTransform(Transform):
432447

433448
def __init__(self, nonpos):
434449
Transform.__init__(self)
435-
if nonpos == 'mask':
436-
self._fill_value = np.nan
437-
else:
438-
self._fill_value = 1e-300
439450
self._nonpos = nonpos
451+
self._clip = {"clip": True, "mask": False}[nonpos]
440452

441453
def transform_non_affine(self, a):
442454
"""logit transform (base 10), masked or clipped"""
443-
with np.errstate(invalid="ignore"):
444-
a = np.select(
445-
[a <= 0, a >= 1], [self._fill_value, 1 - self._fill_value], a)
446-
return np.log10(a / (1 - a))
455+
with np.errstate(divide="ignore", invalid="ignore"):
456+
out = np.log10(a / (1 - a))
457+
if self._clip: # See LogTransform for choice of clip value.
458+
out[a <= 0] = -1000
459+
out[1 <= a] = 1000
460+
return out
447461

448462
def inverted(self):
449463
return LogisticTransform(self._nonpos)
Loading

lib/matplotlib/tests/baseline_images/test_axes/log_scales.svg

Lines changed: 2 additions & 1 deletion
Loading
Loading
Loading
Loading

lib/matplotlib/tests/test_path.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,15 @@ def test_path_clipping():
8787
xy, facecolor='none', edgecolor='red', closed=True))
8888

8989

90-
@image_comparison(baseline_images=['semi_log_with_zero'], extensions=['png'])
90+
@image_comparison(baseline_images=['semi_log_with_zero'], extensions=['png'],
91+
style='mpl20')
9192
def test_log_transform_with_zero():
9293
x = np.arange(-10, 10)
9394
y = (1.0 - 1.0/(x**2+1))**20
9495

9596
fig, ax = plt.subplots()
96-
ax.semilogy(x, y, "-o", lw=15)
97+
ax.semilogy(x, y, "-o", lw=15, markeredgecolor='k')
98+
ax.set_ylim(1e-7, 1)
9799
ax.grid(True)
98100

99101

0 commit comments

Comments
 (0)