Skip to content

BUG: fix minpos handling and other log ticker problems #7598

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

Merged
merged 4 commits into from
Dec 12, 2016
Merged
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
7 changes: 5 additions & 2 deletions doc/api/api_changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ the kwarg is None which internally sets it to the 'auto' string,
triggering a new algorithm for adjusting the maximum according
to the axis length relative to the ticklabel font size.

`matplotlib.ticker.LogFormatter` gains minor_thresholds kwarg
-------------------------------------------------------------
`matplotlib.ticker.LogFormatter`: two new kwargs
------------------------------------------------

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

To improve support for axes using `~matplotlib.ticker.SymmetricLogLocator`,
a 'linthresh' kwarg was added.


New defaults for 3D quiver function in mpl_toolkits.mplot3d.axes3d.py
---------------------------------------------------------------------
Expand Down
9 changes: 8 additions & 1 deletion lib/matplotlib/scale.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,10 @@ def limit_range_for_scale(self, vmin, vmax, minpos):
"""
Limit the domain to positive values.
"""
if not np.isfinite(minpos):
minpos = 1e-300 # This value should rarely if ever
# end up with a visible effect.

return (minpos if vmin <= 0 else vmin,
minpos if vmax <= 0 else vmax)

Expand Down Expand Up @@ -499,7 +503,10 @@ def limit_range_for_scale(self, vmin, vmax, minpos):
"""
Limit the domain to values between 0 and 1 (excluded).
"""
return (minpos if vmin <= 0 else minpos,
if not np.isfinite(minpos):
minpos = 1e-7 # This value should rarely if ever
# end up with a visible effect.
return (minpos if vmin <= 0 else vmin,
1 - minpos if vmax >= 1 else vmax)


Expand Down
19 changes: 19 additions & 0 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,25 @@ def test_autoscale_tight():
assert_allclose(ax.get_xlim(), (-0.15, 3.15))
assert_allclose(ax.get_ylim(), (1.0, 4.0))


@cleanup(style='default')
def test_autoscale_log_shared():
# related to github #7587
# array starts at zero to trigger _minpos handling
x = np.arange(100, dtype=float)
fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True)
ax1.loglog(x, x)
ax2.semilogx(x, x)
ax1.autoscale(tight=True)
ax2.autoscale(tight=True)
plt.draw()
lims = (x[1], x[-1])
assert_allclose(ax1.get_xlim(), lims)
assert_allclose(ax1.get_ylim(), lims)
assert_allclose(ax2.get_xlim(), lims)
assert_allclose(ax2.get_ylim(), (x[0], x[-1]))


@cleanup(style='default')
def test_use_sticky_edges():
fig, ax = plt.subplots()
Expand Down
193 changes: 93 additions & 100 deletions lib/matplotlib/ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -902,10 +902,6 @@ def set_locs(self, locs=None):
self._sublabels = None
return

b = self._base

vmin, vmax = self.axis.get_view_interval()

# Handle symlog case:
linthresh = self._linthresh
if linthresh is None:
Expand All @@ -914,6 +910,18 @@ def set_locs(self, locs=None):
except AttributeError:
pass

vmin, vmax = self.axis.get_view_interval()
if vmin > vmax:
vmin, vmax = vmax, vmin

if linthresh is None and vmin <= 0:
# It's probably a colorbar with
# a format kwarg setting a LogFormatter in the manner
# that worked with 1.5.x, but that doesn't work now.
self._sublabels = set((1,)) # label powers of base
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With Python 2.7, can use a set literal {1}.

return

b = self._base
if linthresh is not None: # symlog
# Only compute the number of decades in the logarithmic part of the
# axis
Expand Down Expand Up @@ -943,37 +951,38 @@ def set_locs(self, locs=None):
# Label all integer multiples of base**n.
self._sublabels = set(np.arange(1, b + 1))

def _num_to_string(self, x, vmin, vmax):
if x > 10000:
s = '%1.0e' % x
elif x < 1:
s = '%1.0e' % x
else:
s = self.pprint_val(x, vmax - vmin)

def __call__(self, x, pos=None):
"""
Return the format for tick val `x`.
"""
vmin, vmax = self.axis.get_view_interval()
vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05)
d = abs(vmax - vmin)
b = self._base
if x == 0.0:
if x == 0.0: # Symlog
return '0'

sign = np.sign(x)
x = abs(x)
b = self._base
# only label the decades
fx = math.log(x) / math.log(b)
is_x_decade = is_close_to_int(fx)
exponent = np.round(fx) if is_x_decade else np.floor(fx)
coeff = np.round(x / b ** exponent)

if self.labelOnlyBase and not is_x_decade:
return ''
if self._sublabels is not None and coeff not in self._sublabels:
return ''

if x > 10000:
s = '%1.0e' % x
elif x < 1:
s = '%1.0e' % x
else:
s = self.pprint_val(x, d)
if sign == -1:
s = '-%s' % s

vmin, vmax = self.axis.get_view_interval()
vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05)
s = self._num_to_string(x, vmin, vmax)
return self.fix_minus(s)

def format_data(self, value):
Expand Down Expand Up @@ -1026,41 +1035,16 @@ class LogFormatterExponent(LogFormatter):
"""
Format values for log axis using ``exponent = log_base(value)``.
"""
def __call__(self, x, pos=None):
"""
Return the format for tick value `x`.
"""
vmin, vmax = self.axis.get_view_interval()
vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05)
d = abs(vmax - vmin)
b = self._base
if x == 0:
return '0'
sign = np.sign(x)
x = abs(x)
# only label the decades
fx = math.log(x) / math.log(b)

is_x_decade = is_close_to_int(fx)
exponent = np.round(fx) if is_x_decade else np.floor(fx)
coeff = np.round(x / b ** exponent)

if self.labelOnlyBase and not is_x_decade:
return ''
if self._sublabels is not None and coeff not in self._sublabels:
return ''

def _num_to_string(self, x, vmin, vmax):
fx = math.log(x) / math.log(self._base)
if abs(fx) > 10000:
s = '%1.0g' % fx
elif abs(fx) < 1:
s = '%1.0g' % fx
else:
fd = math.log(abs(d)) / math.log(b)
fd = math.log(vmax - vmin) / math.log(self._base)
s = self.pprint_val(fx, fd)
if sign == -1:
s = '-%s' % s

return self.fix_minus(s)
return s


class LogFormatterMathtext(LogFormatter):
Expand All @@ -1082,35 +1066,34 @@ def __call__(self, x, pos=None):

The position `pos` is ignored.
"""
b = self._base
usetex = rcParams['text.usetex']

# only label the decades
if x == 0:
if x == 0: # Symlog
if usetex:
return '$0$'
else:
return '$%s$' % _mathdefault('0')

sign_string = '-' if x < 0 else ''
x = abs(x)
b = self._base

# only label the decades
fx = math.log(x) / math.log(b)
is_x_decade = is_close_to_int(fx)
exponent = np.round(fx) if is_x_decade else np.floor(fx)
coeff = np.round(x / b ** exponent)

if self.labelOnlyBase and not is_x_decade:
return ''
if self._sublabels is not None and coeff not in self._sublabels:
return ''

# use string formatting of the base if it is not an integer
if b % 1 == 0.0:
base = '%d' % b
else:
base = '%s' % b

if self.labelOnlyBase and not is_x_decade:
return ''
if self._sublabels is not None and coeff not in self._sublabels:
return ''

if not is_x_decade:
return self._non_decade_format(sign_string, base, fx, usetex)
else:
Expand Down Expand Up @@ -2032,36 +2015,41 @@ def view_limits(self, vmin, vmax):
'Try to choose the view limits intelligently'
b = self._base

if vmax < vmin:
vmin, vmax = vmax, vmin
vmin, vmax = self.nonsingular(vmin, vmax)

if self.axis.axes.name == 'polar':
vmax = math.ceil(math.log(vmax) / math.log(b))
vmin = b ** (vmax - self.numdecs)
return vmin, vmax

minpos = self.axis.get_minpos()

if minpos <= 0 or not np.isfinite(minpos):
raise ValueError(
"Data has no positive values, and therefore can not be "
"log-scaled.")

if vmin <= 0:
vmin = minpos

if rcParams['axes.autolimit_mode'] == 'round_numbers':
if not is_decade(vmin, self._base):
vmin = decade_down(vmin, self._base)
if not is_decade(vmax, self._base):
vmax = decade_up(vmax, self._base)

if vmin == vmax:
vmin = decade_down(vmin, self._base)
vmax = decade_up(vmax, self._base)
return vmin, vmax

result = mtransforms.nonsingular(vmin, vmax)
return result
def nonsingular(self, vmin, vmax):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we already have have a nonsingular?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but 'nonsingular' means different things for different transforms--different behaviors are needed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this have a leading underscore? I missread and did not notice that this was a method.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could, but there are other nonsingular methods like this that don't, and they are called as locator.nonsingular(x0, x1) in autoscale_view(). So if I were to add an underscore here, I would either have to use two different calls in autoscale_view, or add an underscore to the other locator methods with that name.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am sold on consistency of naming.

if not np.isfinite(vmin) or not np.isfinite(vmax):
return 1, 10 # initial range, no data plotted yet

if vmin > vmax:
vmin, vmax = vmax, vmin
if vmax <= 0:
warnings.warn(
"Data has no positive values, and therefore cannot be "
"log-scaled.")
return 1, 10

minpos = self.axis.get_minpos()
if not np.isfinite(minpos):
minpos = 1e-300 # This should never take effect.
if vmin <= 0:
vmin = minpos
if vmin == vmax:
vmin = decade_down(vmin, self._base)
vmax = decade_up(vmax, self._base)
return vmin, vmax


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

# what to do if a window beyond ]0, 1[ is chosen
if vmin <= 0.0:
if self.axis is not None:
vmin = self.axis.get_minpos()

if (vmin <= 0.0) or (not np.isfinite(vmin)):
raise ValueError(
"Data has no values in ]0, 1[ and therefore can not be "
"logit-scaled.")

# NOTE: for vmax, we should query a property similar to get_minpos, but
# related to the maximal, less-than-one data point. Unfortunately,
# get_minpos is defined very deep in the BBox and updated with data,
# so for now we use the trick below.
if vmax >= 1.0:
if self.axis is not None:
vmax = 1 - self.axis.get_minpos()

if (vmax >= 1.0) or (not np.isfinite(vmax)):
raise ValueError(
"Data has no values in ]0, 1[ and therefore can not be "
"logit-scaled.")

if vmax < vmin:
vmin, vmax = vmax, vmin

vmin, vmax = self.nonsingular(vmin, vmax)
vmin = np.log10(vmin / (1 - vmin))
vmax = np.log10(vmax / (1 - vmax))

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

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

def nonsingular(self, vmin, vmax):
initial_range = (1e-7, 1 - 1e-7)
if not np.isfinite(vmin) or not np.isfinite(vmax):
return initial_range # no data plotted yet

if vmin > vmax:
vmin, vmax = vmax, vmin

# what to do if a window beyond ]0, 1[ is chosen
if self.axis is not None:
minpos = self.axis.get_minpos()
if not np.isfinite(minpos):
return initial_range # again, no data plotted
else:
minpos = 1e-7 # should not occur in normal use

# NOTE: for vmax, we should query a property similar to get_minpos, but
# related to the maximal, less-than-one data point. Unfortunately,
# Bbox._minpos is defined very deep in the BBox and updated with data,
# so for now we use 1 - minpos as a substitute.

if vmin <= 0:
vmin = minpos
if vmax >= 1:
vmax = 1 - minpos
if vmin == vmax:
return 0.1 * vmin, 1 - 0.1 * vmin

return vmin, vmax


class AutoLocator(MaxNLocator):
def __init__(self):
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,7 @@ def __init__(self, points, **kwargs):
raise ValueError('Bbox points must be of the form '
'"[[x0, y0], [x1, y1]]".')
self._points = points
self._minpos = np.array([0.0000001, 0.0000001])
self._minpos = np.array([np.inf, np.inf])
self._ignore = True
# it is helpful in some contexts to know if the bbox is a
# default or has been mutated; we store the orig points to
Expand Down