Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 4d59447

Browse filesBrowse 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
Copy full SHA for 4d59447

File tree

Expand file treeCollapse file tree

5 files changed

+126
-104
lines changed
Filter options
Expand file treeCollapse file tree

5 files changed

+126
-104
lines changed

‎doc/api/api_changes.rst

Copy file name to clipboardExpand all lines: doc/api/api_changes.rst
+5-2Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/scale.py
+8-1Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_axes.py
+19Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/ticker.py
+93-100Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/transforms.py
+1-1Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.