Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 4d59447

Browse files
authored
Merge pull request #7598 from efiring/minpos_initial
BUG: fix minpos handling and other log ticker problems
2 parents 4177725 + aa36ec3 commit 4d59447

File tree

5 files changed

+126
-104
lines changed

5 files changed

+126
-104
lines changed

doc/api/api_changes.rst

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ the kwarg is None which internally sets it to the 'auto' string,
141141
triggering a new algorithm for adjusting the maximum according
142142
to the axis length relative to the ticklabel font size.
143143

144-
`matplotlib.ticker.LogFormatter` gains minor_thresholds kwarg
145-
-------------------------------------------------------------
144+
`matplotlib.ticker.LogFormatter`: two new kwargs
145+
------------------------------------------------
146146

147147
Previously, minor ticks on log-scaled axes were not labeled by
148148
default. An algorithm has been added to the
@@ -151,6 +151,9 @@ ticks between integer powers of the base. The algorithm uses
151151
two parameters supplied in a kwarg tuple named 'minor_thresholds'.
152152
See the docstring for further explanation.
153153

154+
To improve support for axes using `~matplotlib.ticker.SymmetricLogLocator`,
155+
a 'linthresh' kwarg was added.
156+
154157

155158
New defaults for 3D quiver function in mpl_toolkits.mplot3d.axes3d.py
156159
---------------------------------------------------------------------

lib/matplotlib/scale.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,10 @@ def limit_range_for_scale(self, vmin, vmax, minpos):
264264
"""
265265
Limit the domain to positive values.
266266
"""
267+
if not np.isfinite(minpos):
268+
minpos = 1e-300 # This value should rarely if ever
269+
# end up with a visible effect.
270+
267271
return (minpos if vmin <= 0 else vmin,
268272
minpos if vmax <= 0 else vmax)
269273

@@ -499,7 +503,10 @@ def limit_range_for_scale(self, vmin, vmax, minpos):
499503
"""
500504
Limit the domain to values between 0 and 1 (excluded).
501505
"""
502-
return (minpos if vmin <= 0 else minpos,
506+
if not np.isfinite(minpos):
507+
minpos = 1e-7 # This value should rarely if ever
508+
# end up with a visible effect.
509+
return (minpos if vmin <= 0 else vmin,
503510
1 - minpos if vmax >= 1 else vmax)
504511

505512

lib/matplotlib/tests/test_axes.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,25 @@ def test_autoscale_tight():
176176
assert_allclose(ax.get_xlim(), (-0.15, 3.15))
177177
assert_allclose(ax.get_ylim(), (1.0, 4.0))
178178

179+
180+
@cleanup(style='default')
181+
def test_autoscale_log_shared():
182+
# related to github #7587
183+
# array starts at zero to trigger _minpos handling
184+
x = np.arange(100, dtype=float)
185+
fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True)
186+
ax1.loglog(x, x)
187+
ax2.semilogx(x, x)
188+
ax1.autoscale(tight=True)
189+
ax2.autoscale(tight=True)
190+
plt.draw()
191+
lims = (x[1], x[-1])
192+
assert_allclose(ax1.get_xlim(), lims)
193+
assert_allclose(ax1.get_ylim(), lims)
194+
assert_allclose(ax2.get_xlim(), lims)
195+
assert_allclose(ax2.get_ylim(), (x[0], x[-1]))
196+
197+
179198
@cleanup(style='default')
180199
def test_use_sticky_edges():
181200
fig, ax = plt.subplots()

lib/matplotlib/ticker.py

Lines changed: 93 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -902,10 +902,6 @@ def set_locs(self, locs=None):
902902
self._sublabels = None
903903
return
904904

905-
b = self._base
906-
907-
vmin, vmax = self.axis.get_view_interval()
908-
909905
# Handle symlog case:
910906
linthresh = self._linthresh
911907
if linthresh is None:
@@ -914,6 +910,18 @@ def set_locs(self, locs=None):
914910
except AttributeError:
915911
pass
916912

913+
vmin, vmax = self.axis.get_view_interval()
914+
if vmin > vmax:
915+
vmin, vmax = vmax, vmin
916+
917+
if linthresh is None and vmin <= 0:
918+
# It's probably a colorbar with
919+
# a format kwarg setting a LogFormatter in the manner
920+
# that worked with 1.5.x, but that doesn't work now.
921+
self._sublabels = set((1,)) # label powers of base
922+
return
923+
924+
b = self._base
917925
if linthresh is not None: # symlog
918926
# Only compute the number of decades in the logarithmic part of the
919927
# axis
@@ -943,37 +951,38 @@ def set_locs(self, locs=None):
943951
# Label all integer multiples of base**n.
944952
self._sublabels = set(np.arange(1, b + 1))
945953

954+
def _num_to_string(self, x, vmin, vmax):
955+
if x > 10000:
956+
s = '%1.0e' % x
957+
elif x < 1:
958+
s = '%1.0e' % x
959+
else:
960+
s = self.pprint_val(x, vmax - vmin)
961+
946962
def __call__(self, x, pos=None):
947963
"""
948964
Return the format for tick val `x`.
949965
"""
950-
vmin, vmax = self.axis.get_view_interval()
951-
vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05)
952-
d = abs(vmax - vmin)
953-
b = self._base
954-
if x == 0.0:
966+
if x == 0.0: # Symlog
955967
return '0'
968+
956969
sign = np.sign(x)
957970
x = abs(x)
971+
b = self._base
958972
# only label the decades
959973
fx = math.log(x) / math.log(b)
960974
is_x_decade = is_close_to_int(fx)
961975
exponent = np.round(fx) if is_x_decade else np.floor(fx)
962976
coeff = np.round(x / b ** exponent)
977+
963978
if self.labelOnlyBase and not is_x_decade:
964979
return ''
965980
if self._sublabels is not None and coeff not in self._sublabels:
966981
return ''
967982

968-
if x > 10000:
969-
s = '%1.0e' % x
970-
elif x < 1:
971-
s = '%1.0e' % x
972-
else:
973-
s = self.pprint_val(x, d)
974-
if sign == -1:
975-
s = '-%s' % s
976-
983+
vmin, vmax = self.axis.get_view_interval()
984+
vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05)
985+
s = self._num_to_string(x, vmin, vmax)
977986
return self.fix_minus(s)
978987

979988
def format_data(self, value):
@@ -1026,41 +1035,16 @@ class LogFormatterExponent(LogFormatter):
10261035
"""
10271036
Format values for log axis using ``exponent = log_base(value)``.
10281037
"""
1029-
def __call__(self, x, pos=None):
1030-
"""
1031-
Return the format for tick value `x`.
1032-
"""
1033-
vmin, vmax = self.axis.get_view_interval()
1034-
vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05)
1035-
d = abs(vmax - vmin)
1036-
b = self._base
1037-
if x == 0:
1038-
return '0'
1039-
sign = np.sign(x)
1040-
x = abs(x)
1041-
# only label the decades
1042-
fx = math.log(x) / math.log(b)
1043-
1044-
is_x_decade = is_close_to_int(fx)
1045-
exponent = np.round(fx) if is_x_decade else np.floor(fx)
1046-
coeff = np.round(x / b ** exponent)
1047-
1048-
if self.labelOnlyBase and not is_x_decade:
1049-
return ''
1050-
if self._sublabels is not None and coeff not in self._sublabels:
1051-
return ''
1052-
1038+
def _num_to_string(self, x, vmin, vmax):
1039+
fx = math.log(x) / math.log(self._base)
10531040
if abs(fx) > 10000:
10541041
s = '%1.0g' % fx
10551042
elif abs(fx) < 1:
10561043
s = '%1.0g' % fx
10571044
else:
1058-
fd = math.log(abs(d)) / math.log(b)
1045+
fd = math.log(vmax - vmin) / math.log(self._base)
10591046
s = self.pprint_val(fx, fd)
1060-
if sign == -1:
1061-
s = '-%s' % s
1062-
1063-
return self.fix_minus(s)
1047+
return s
10641048

10651049

10661050
class LogFormatterMathtext(LogFormatter):
@@ -1082,35 +1066,34 @@ def __call__(self, x, pos=None):
10821066
10831067
The position `pos` is ignored.
10841068
"""
1085-
b = self._base
10861069
usetex = rcParams['text.usetex']
1087-
1088-
# only label the decades
1089-
if x == 0:
1070+
if x == 0: # Symlog
10901071
if usetex:
10911072
return '$0$'
10921073
else:
10931074
return '$%s$' % _mathdefault('0')
10941075

10951076
sign_string = '-' if x < 0 else ''
10961077
x = abs(x)
1078+
b = self._base
10971079

1080+
# only label the decades
10981081
fx = math.log(x) / math.log(b)
10991082
is_x_decade = is_close_to_int(fx)
11001083
exponent = np.round(fx) if is_x_decade else np.floor(fx)
11011084
coeff = np.round(x / b ** exponent)
11021085

1086+
if self.labelOnlyBase and not is_x_decade:
1087+
return ''
1088+
if self._sublabels is not None and coeff not in self._sublabels:
1089+
return ''
1090+
11031091
# use string formatting of the base if it is not an integer
11041092
if b % 1 == 0.0:
11051093
base = '%d' % b
11061094
else:
11071095
base = '%s' % b
11081096

1109-
if self.labelOnlyBase and not is_x_decade:
1110-
return ''
1111-
if self._sublabels is not None and coeff not in self._sublabels:
1112-
return ''
1113-
11141097
if not is_x_decade:
11151098
return self._non_decade_format(sign_string, base, fx, usetex)
11161099
else:
@@ -2032,36 +2015,41 @@ def view_limits(self, vmin, vmax):
20322015
'Try to choose the view limits intelligently'
20332016
b = self._base
20342017

2035-
if vmax < vmin:
2036-
vmin, vmax = vmax, vmin
2018+
vmin, vmax = self.nonsingular(vmin, vmax)
20372019

20382020
if self.axis.axes.name == 'polar':
20392021
vmax = math.ceil(math.log(vmax) / math.log(b))
20402022
vmin = b ** (vmax - self.numdecs)
2041-
return vmin, vmax
2042-
2043-
minpos = self.axis.get_minpos()
2044-
2045-
if minpos <= 0 or not np.isfinite(minpos):
2046-
raise ValueError(
2047-
"Data has no positive values, and therefore can not be "
2048-
"log-scaled.")
2049-
2050-
if vmin <= 0:
2051-
vmin = minpos
20522023

20532024
if rcParams['axes.autolimit_mode'] == 'round_numbers':
20542025
if not is_decade(vmin, self._base):
20552026
vmin = decade_down(vmin, self._base)
20562027
if not is_decade(vmax, self._base):
20572028
vmax = decade_up(vmax, self._base)
20582029

2059-
if vmin == vmax:
2060-
vmin = decade_down(vmin, self._base)
2061-
vmax = decade_up(vmax, self._base)
2030+
return vmin, vmax
20622031

2063-
result = mtransforms.nonsingular(vmin, vmax)
2064-
return result
2032+
def nonsingular(self, vmin, vmax):
2033+
if not np.isfinite(vmin) or not np.isfinite(vmax):
2034+
return 1, 10 # initial range, no data plotted yet
2035+
2036+
if vmin > vmax:
2037+
vmin, vmax = vmax, vmin
2038+
if vmax <= 0:
2039+
warnings.warn(
2040+
"Data has no positive values, and therefore cannot be "
2041+
"log-scaled.")
2042+
return 1, 10
2043+
2044+
minpos = self.axis.get_minpos()
2045+
if not np.isfinite(minpos):
2046+
minpos = 1e-300 # This should never take effect.
2047+
if vmin <= 0:
2048+
vmin = minpos
2049+
if vmin == vmax:
2050+
vmin = decade_down(vmin, self._base)
2051+
vmax = decade_up(vmax, self._base)
2052+
return vmin, vmax
20652053

20662054

20672055
class SymmetricalLogLocator(Locator):
@@ -2260,32 +2248,7 @@ def tick_values(self, vmin, vmax):
22602248
if hasattr(self.axis, 'axes') and self.axis.axes.name == 'polar':
22612249
raise NotImplementedError('Polar axis cannot be logit scaled yet')
22622250

2263-
# what to do if a window beyond ]0, 1[ is chosen
2264-
if vmin <= 0.0:
2265-
if self.axis is not None:
2266-
vmin = self.axis.get_minpos()
2267-
2268-
if (vmin <= 0.0) or (not np.isfinite(vmin)):
2269-
raise ValueError(
2270-
"Data has no values in ]0, 1[ and therefore can not be "
2271-
"logit-scaled.")
2272-
2273-
# NOTE: for vmax, we should query a property similar to get_minpos, but
2274-
# related to the maximal, less-than-one data point. Unfortunately,
2275-
# get_minpos is defined very deep in the BBox and updated with data,
2276-
# so for now we use the trick below.
2277-
if vmax >= 1.0:
2278-
if self.axis is not None:
2279-
vmax = 1 - self.axis.get_minpos()
2280-
2281-
if (vmax >= 1.0) or (not np.isfinite(vmax)):
2282-
raise ValueError(
2283-
"Data has no values in ]0, 1[ and therefore can not be "
2284-
"logit-scaled.")
2285-
2286-
if vmax < vmin:
2287-
vmin, vmax = vmax, vmin
2288-
2251+
vmin, vmax = self.nonsingular(vmin, vmax)
22892252
vmin = np.log10(vmin / (1 - vmin))
22902253
vmax = np.log10(vmax / (1 - vmax))
22912254

@@ -2320,6 +2283,36 @@ def tick_values(self, vmin, vmax):
23202283

23212284
return self.raise_if_exceeds(np.array(ticklocs))
23222285

2286+
def nonsingular(self, vmin, vmax):
2287+
initial_range = (1e-7, 1 - 1e-7)
2288+
if not np.isfinite(vmin) or not np.isfinite(vmax):
2289+
return initial_range # no data plotted yet
2290+
2291+
if vmin > vmax:
2292+
vmin, vmax = vmax, vmin
2293+
2294+
# what to do if a window beyond ]0, 1[ is chosen
2295+
if self.axis is not None:
2296+
minpos = self.axis.get_minpos()
2297+
if not np.isfinite(minpos):
2298+
return initial_range # again, no data plotted
2299+
else:
2300+
minpos = 1e-7 # should not occur in normal use
2301+
2302+
# NOTE: for vmax, we should query a property similar to get_minpos, but
2303+
# related to the maximal, less-than-one data point. Unfortunately,
2304+
# Bbox._minpos is defined very deep in the BBox and updated with data,
2305+
# so for now we use 1 - minpos as a substitute.
2306+
2307+
if vmin <= 0:
2308+
vmin = minpos
2309+
if vmax >= 1:
2310+
vmax = 1 - minpos
2311+
if vmin == vmax:
2312+
return 0.1 * vmin, 1 - 0.1 * vmin
2313+
2314+
return vmin, vmax
2315+
23232316

23242317
class AutoLocator(MaxNLocator):
23252318
def __init__(self):

lib/matplotlib/transforms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -792,7 +792,7 @@ def __init__(self, points, **kwargs):
792792
raise ValueError('Bbox points must be of the form '
793793
'"[[x0, y0], [x1, y1]]".')
794794
self._points = points
795-
self._minpos = np.array([0.0000001, 0.0000001])
795+
self._minpos = np.array([np.inf, np.inf])
796796
self._ignore = True
797797
# it is helpful in some contexts to know if the bbox is a
798798
# default or has been mutated; we store the orig points to

0 commit comments

Comments
 (0)