Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Locator interface #1064

Merged
merged 6 commits into from

3 participants

@pelson
Collaborator

I have a need to get ticks for a given range, and the matplotlib Locator seemed like the perfect fit. Unfortunately, the Locator class needs an Axis to function (even if that Axis is a DummyAxis instance).

This PR makes it easy for some Locators to expose their underlying cleverness without the need for an Axis instance.

@mdboom
Owner

Looks good. Maybe add a unit test and example of how to use this in the new way (without an Axes)? Not sure where that example should go -- it's non-visual (well, non-plot-producing) so it might be hard to find.

@efiring efiring commented on the diff
lib/matplotlib/ticker.py
@@ -913,7 +935,17 @@ def __init__(self, locs, nbins=None):
self.nbins = max(self.nbins, 2)
def __call__(self):
- 'Return the locations of the ticks'
+ return self.tick_values(None, None)
+
+ def tick_values(self, vmin, vmax):
@efiring Owner
efiring added a note

vmin -> dmin etc. for consistency

@pelson Collaborator
pelson added a note

For self consistency I would change all signatures in this module from dmin to vmin. Is there a consistency outside of this file which would make dmin preferable?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@efiring
Owner

I like this idea--I never liked the dummy axis.
Is there any reason not to remove most or all of the separate call definitions? It looks like they could be inherited from the base class.

lib/matplotlib/ticker.py
((22 lines not shown))
def __call__(self):
- 'Return the locations of the ticks'
+ """Return the locations of the ticks"""
+ # note: some locators return data limits, other return view limits,
+ # hence there is no *one* interface to call self.tick_values.
@pelson Collaborator
pelson added a note

@efiring said:

Is there any reason not to remove most or all of the separate call definitions? It looks like they could be inherited from the base class.

Unfortunately some __call__ methods use self.axis.get_view_interval others use self.axis.get_data_interval() so I couldn't comfortably make that change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@pelson
Collaborator

Looks good. Maybe add a unit test and example of how to use this in the new way (without an Axes)?

Yes, I'd be happy to put in a unit test, but not sure a usage example would be that helpful/interesting.

@pelson
Collaborator

Ok. I've just added some tests for some of the Locators, but have not updated the "whats new" section as I am not sure such a menial thing is worthy of such exposure. Happy to make any further alterations though if either of you feel it is necessary.

@pelson
Collaborator

Will just rebase before it gets reviewed.

@mdboom
Owner

The example I was thinking of would just be something to show how, given a range, one could get a list of ticks back. You've made it much easier than before, so we should show how easy it is... ;)

@pelson
Collaborator

Ok. changelog added.

@efiring efiring merged commit 4526865 into matplotlib:master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
12 doc/users/whats_new.rst
@@ -17,6 +17,18 @@ This page just covers the highlights -- for the full story, see the
new in matplotlib-1.2
=====================
+Locator interface
+-----------------
+
+Philip Elson exposed the intelligence behind the tick Locator classes with a
+simple interface. For instance, to get no more than 5 sensible steps which
+span the values 10 and 19.5::
+
+ >>> import matplotlib.ticker as mticker
+ >>> locator = mticker.MaxNLocator(nbins=5)
+ >>> print(locator.tick_values(10, 19.5))
+ [ 10. 12. 14. 16. 18. 20.]
+
Tri-Surface Plots
-----------------
View
22 lib/matplotlib/__init__.py
@@ -1003,26 +1003,28 @@ def tk_window_focus():
default_test_modules = [
'matplotlib.tests.test_agg',
+ 'matplotlib.tests.test_axes',
'matplotlib.tests.test_backend_svg',
'matplotlib.tests.test_basic',
'matplotlib.tests.test_cbook',
- 'matplotlib.tests.test_mlab',
- 'matplotlib.tests.test_transforms',
- 'matplotlib.tests.test_axes',
- 'matplotlib.tests.test_figure',
+ 'matplotlib.tests.test_colorbar',
'matplotlib.tests.test_dates',
- 'matplotlib.tests.test_spines',
+ 'matplotlib.tests.test_delaunay',
+ 'matplotlib.tests.test_figure',
'matplotlib.tests.test_image',
- 'matplotlib.tests.test_simplification',
+ 'matplotlib.tests.test_legend',
'matplotlib.tests.test_mathtext',
+ 'matplotlib.tests.test_mlab',
+ 'matplotlib.tests.test_patches',
+ 'matplotlib.tests.test_simplification',
+ 'matplotlib.tests.test_spines',
'matplotlib.tests.test_text',
+ 'matplotlib.tests.test_ticker',
'matplotlib.tests.test_tightlayout',
- 'matplotlib.tests.test_delaunay',
- 'matplotlib.tests.test_legend',
- 'matplotlib.tests.test_colorbar',
- 'matplotlib.tests.test_patches',
+ 'matplotlib.tests.test_transforms',
]
+
def test(verbosity=1):
"""run the matplotlib test suite"""
old_backend = rcParams['backend']
View
4 lib/matplotlib/contour.py
@@ -999,11 +999,9 @@ def _autolev(self, z, N):
self.locator = ticker.LogLocator()
else:
self.locator = ticker.MaxNLocator(N+1)
- self.locator.create_dummy_axis()
zmax = self.zmax
zmin = self.zmin
- self.locator.set_bounds(zmin, zmax)
- lev = self.locator()
+ lev = self.locator.tick_values(zmin, zmax)
self._auto = True
if self.filled:
return lev
View
51 lib/matplotlib/tests/test_ticker.py
@@ -0,0 +1,51 @@
+from __future__ import print_function
+from nose.tools import assert_equal, assert_raises
+from numpy.testing import assert_almost_equal
+import numpy as np
+
+import matplotlib.ticker as mticker
+
+
+def test_MaxNLocator():
+ loc = mticker.MaxNLocator(nbins=5)
+ test_value = np.array([ 20., 40., 60., 80., 100.])
+ assert_almost_equal(loc.tick_values(20, 100), test_value)
+
+ test_value = np.array([ 0., 0.0002, 0.0004, 0.0006, 0.0008, 0.001])
+ assert_almost_equal(loc.tick_values(0.001, 0.0001), test_value)
+
+ test_value = np.array([-1.0e+15, -5.0e+14, 0e+00, 5e+14, 1.0e+15])
+ assert_almost_equal(loc.tick_values(-1e15, 1e15), test_value)
+
+
+def test_LinearLocator():
+ loc = mticker.LinearLocator(numticks=3)
+ test_value = np.array([-0.8, -0.3, 0.2])
+ assert_almost_equal(loc.tick_values(-0.8, 0.2), test_value)
+
+
+def test_MultipleLocator():
+ loc = mticker.MultipleLocator(base=3.147)
+ test_value = np.array([-6.294, -3.147, 0., 3.147, 6.294, 9.441])
+ assert_almost_equal(loc.tick_values(-7, 10), test_value)
+
+
+def test_LogLocator():
+ loc = mticker.LogLocator(numticks=5)
+
+ # make sure the 0 case is covered with an exception
+ with assert_raises(ValueError):
+ loc.tick_values(0, 1000)
+
+ test_value = np.array([1.00000000e-03, 1.00000000e-01, 1.00000000e+01,
+ 1.00000000e+03, 1.00000000e+05, 1.00000000e+07])
+ assert_almost_equal(loc.tick_values(0.001, 1.1e5), test_value)
+
+ loc = mticker.LogLocator(base=2)
+ test_value = np.array([1., 2., 4., 8., 16., 32., 64., 128.])
+ assert_almost_equal(loc.tick_values(1, 100), test_value)
+
+
+if __name__=='__main__':
+ import nose
+ nose.runmodule(argv=['-s','--with-doctest'], exit=False)
View
123 lib/matplotlib/ticker.py
@@ -182,7 +182,7 @@ class Formatter(TickHelper):
locs = []
def __call__(self, x, pos=None):
'Return the format for tick val x at position pos; pos=None indicated unspecified'
- raise NotImplementedError('Derived must overide')
+ raise NotImplementedError('Derived must override')
def format_data(self,value):
return self.__call__(value)
@@ -819,12 +819,32 @@ class Locator(TickHelper):
# This parameter is set to cause locators to raise an error if too
# many ticks are generated
MAXTICKS = 1000
+
+ def tick_values(self, vmin, vmax):
+ """
+ Return the values of the located ticks given **vmin** and **vmax**.
+
+ .. note::
+ To get tick locations with the vmin and vmax values defined
+ automatically for the associated :attr:`axis` simply call
+ the Locator instance::
+
+ >>> print(type(loc))
+ <type 'Locator'>
+ >>> print(loc())
+ [1, 2, 3, 4]
+
+ """
+ raise NotImplementedError('Derived must override')
+
def __call__(self):
- 'Return the locations of the ticks'
+ """Return the locations of the ticks"""
+ # note: some locators return data limits, other return view limits,
+ # hence there is no *one* interface to call self.tick_values.
raise NotImplementedError('Derived must override')
def raise_if_exceeds(self, locs):
- 'raise a RuntimeError if Locator attempts to create more than MAXTICKS locs'
+ """raise a RuntimeError if Locator attempts to create more than MAXTICKS locs"""
if len(locs)>=self.MAXTICKS:
msg = ('Locator attempting to generate %d ticks from %s to %s: ' +
'exceeds Locator.MAXTICKS') % (len(locs), locs[0], locs[-1])
@@ -841,11 +861,11 @@ def view_limits(self, vmin, vmax):
return mtransforms.nonsingular(vmin, vmax)
def autoscale(self):
- 'autoscale the view limits'
+ """autoscale the view limits"""
return self.view_limits(*self.axis.get_view_interval())
def pan(self, numsteps):
- 'Pan numticks (can be positive or negative)'
+ """Pan numticks (can be positive or negative)"""
ticks = self()
numticks = len(ticks)
@@ -861,7 +881,6 @@ def pan(self, numsteps):
vmax += step
self.axis.set_view_interval(vmin, vmax, ignore=True)
-
def zoom(self, direction):
"Zoom in/out on axis; if direction is >0 zoom in, else zoom out"
@@ -872,7 +891,7 @@ def zoom(self, direction):
self.axis.set_view_interval(vmin + step, vmax - step, ignore=True)
def refresh(self):
- 'refresh internal information based on current lim'
+ """refresh internal information based on current lim"""
pass
@@ -889,10 +908,13 @@ def __init__(self, base, offset):
self.offset = offset
def __call__(self):
- 'Return the locations of the ticks'
+ """Return the locations of the ticks"""
dmin, dmax = self.axis.get_data_interval()
+ return self.tick_values(dmin, dmax)
+
+ def tick_values(self, vmin, vmax):
return self.raise_if_exceeds(
- np.arange(dmin + self.offset, dmax+1, self._base))
+ np.arange(vmin + self.offset, vmax+1, self._base))
class FixedLocator(Locator):
@@ -913,7 +935,17 @@ def __init__(self, locs, nbins=None):
self.nbins = max(self.nbins, 2)
def __call__(self):
- 'Return the locations of the ticks'
+ return self.tick_values(None, None)
+
+ def tick_values(self, vmin, vmax):
@efiring Owner
efiring added a note

vmin -> dmin etc. for consistency

@pelson Collaborator
pelson added a note

For self consistency I would change all signatures in this module from dmin to vmin. Is there a consistency outside of this file which would make dmin preferable?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ """"
+ Return the locations of the ticks.
+
+ .. note::
+
+ Because the values are fixed, vmin and vmax are not used in this method.
+
+ """
if self.nbins is None:
return self.locs
step = max(int(0.99 + len(self.locs) / float(self.nbins)), 1)
@@ -925,17 +957,26 @@ def __call__(self):
return self.raise_if_exceeds(ticks)
-
-
class NullLocator(Locator):
"""
No ticks
"""
def __call__(self):
- 'Return the locations of the ticks'
+ return self.tick_values(None, None)
+
+ def tick_values(self, vmin, vmax):
+ """"
+ Return the locations of the ticks.
+
+ .. note::
+
+ Because the values are Null, vmin and vmax are not used in this method.
+
+ """
return []
+
class LinearLocator(Locator):
"""
Determine the tick locations
@@ -944,9 +985,8 @@ class LinearLocator(Locator):
number of ticks to make a nice tick partitioning. Thereafter the
number of ticks will be fixed so that interactive navigation will
be nice
- """
-
+ """
def __init__(self, numticks = None, presets=None):
"""
Use presets to set locs based on lom. A dict mapping vmin, vmax->locs
@@ -959,8 +999,10 @@ def __init__(self, numticks = None, presets=None):
def __call__(self):
'Return the locations of the ticks'
-
vmin, vmax = self.axis.get_view_interval()
+ return self.tick_values(vmin, vmax)
+
+ def tick_values(self, vmin, vmax):
vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander = 0.05)
if vmax<vmin:
vmin, vmax = vmax, vmin
@@ -971,14 +1013,11 @@ def __call__(self):
if self.numticks is None:
self._set_numticks()
-
-
if self.numticks==0: return []
ticklocs = np.linspace(vmin, vmax, self.numticks)
return self.raise_if_exceeds(ticklocs)
-
def _set_numticks(self):
self.numticks = 11 # todo; be smart here; this is just for dev
@@ -1007,6 +1046,7 @@ def closeto(x,y):
if abs(x-y)<1e-10: return True
else: return False
+
class Base:
'this solution has some hacks to deal with floating point inaccuracies'
def __init__(self, base):
@@ -1046,6 +1086,7 @@ def ge(self, x):
def get_base(self):
return self._base
+
class MultipleLocator(Locator):
"""
Set a tick on every integer that is multiple of base in the
@@ -1058,6 +1099,9 @@ def __init__(self, base=1.0):
def __call__(self):
'Return the locations of the ticks'
vmin, vmax = self.axis.get_view_interval()
+ return self.tick_values(vmin, vmax)
+
+ def tick_values(self, vmin, vmax):
if vmax<vmin:
vmin, vmax = vmax, vmin
vmin = self._base.ge(vmin)
@@ -1206,9 +1250,11 @@ def bin_boundaries(self, vmin, vmax):
nbins -= extra_bins
return (np.arange(nbins+1) * step + best_vmin + offset)
-
def __call__(self):
vmin, vmax = self.axis.get_view_interval()
+ return self.tick_values(vmin, vmax)
+
+ def tick_values(self, vmin, vmax):
vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander = 1e-13,
tiny=1e-14)
locs = self.bin_boundaries(vmin, vmax)
@@ -1238,6 +1284,7 @@ def decade_down(x, base=10):
lx = np.floor(np.log(x)/np.log(base))
return base**lx
+
def decade_up(x, base=10):
'ceil x to the nearest higher decade'
if x == 0.0:
@@ -1245,11 +1292,13 @@ def decade_up(x, base=10):
lx = np.ceil(np.log(x)/np.log(base))
return base**lx
+
def nearest_long(x):
if x == 0: return 0L
elif x > 0: return long(x+0.5)
else: return long(x-0.5)
+
def is_decade(x, base=10):
if not np.isfinite(x):
return False
@@ -1258,23 +1307,25 @@ def is_decade(x, base=10):
lx = np.log(np.abs(x))/np.log(base)
return is_close_to_int(lx)
+
def is_close_to_int(x):
if not np.isfinite(x):
return False
return abs(x - nearest_long(x)) < 1e-10
+
class LogLocator(Locator):
"""
Determine the tick locations for log axes
"""
- def __init__(self, base=10.0, subs=[1.0], numdecs=4):
+ def __init__(self, base=10.0, subs=[1.0], numdecs=4, numticks=15):
"""
place ticks on the location= base**i*subs[j]
"""
self.base(base)
self.subs(subs)
- self.numticks = 15
+ self.numticks = numticks
self.numdecs = numdecs
def base(self,base):
@@ -1294,10 +1345,11 @@ def subs(self,subs):
def __call__(self):
'Return the locations of the ticks'
- b=self._base
-
vmin, vmax = self.axis.get_view_interval()
+ return self.tick_values(vmin, vmax)
+ def tick_values(self, vmin, vmax):
+ b=self._base
# dummy axis has no axes attribute
if hasattr(self.axis, 'axes') and self.axis.axes.name == 'polar':
vmax = math.ceil(math.log(vmax) / math.log(b))
@@ -1307,7 +1359,9 @@ def __call__(self):
return ticklocs
if vmin <= 0.0:
- vmin = self.axis.get_minpos()
+ 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 positive values, and therefore can not be log-scaled.")
@@ -1377,6 +1431,7 @@ def view_limits(self, vmin, vmax):
result = mtransforms.nonsingular(vmin, vmax)
return result
+
class SymmetricalLogLocator(Locator):
"""
Determine the tick locations for log axes
@@ -1395,11 +1450,14 @@ def __init__(self, transform, subs=None):
def __call__(self):
'Return the locations of the ticks'
+ # Note, these are untransformed coordinates
+ vmin, vmax = self.axis.get_view_interval()
+ return self.tick_values(vmin, vmax)
+
+ def tick_values(self, vmin, vmax):
b = self._transform.base
t = self._transform.linthresh
- # Note, these are untransformed coordinates
- vmin, vmax = self.axis.get_view_interval()
if vmax < vmin:
vmin, vmax = vmax, vmin
@@ -1525,10 +1583,12 @@ def view_limits(self, vmin, vmax):
result = mtransforms.nonsingular(vmin, vmax)
return result
+
class AutoLocator(MaxNLocator):
def __init__(self):
MaxNLocator.__init__(self, nbins=9, steps=[1, 2, 5, 10])
+
class AutoMinorLocator(Locator):
"""
Dynamically find minor tick positions based on the positions of
@@ -1588,6 +1648,10 @@ def __call__(self):
return self.raise_if_exceeds(np.array(locs))
+ def tick_values(self, vmin, vmax):
+ raise NotImplementedError('Cannot get tick locations for a '
+ '%s type.' % type(self))
+
class OldAutoLocator(Locator):
"""
@@ -1603,6 +1667,10 @@ def __call__(self):
self.refresh()
return self.raise_if_exceeds(self._locator())
+ def tick_values(self, vmin, vmax):
+ raise NotImplementedError('Cannot get tick locations for a '
+ '%s type.' % type(self))
+
def refresh(self):
'refresh internal information based on current lim'
vmin, vmax = self.axis.get_view_interval()
@@ -1644,7 +1712,6 @@ def get_locator(self, d):
return locator
-
__all__ = ('TickHelper', 'Formatter', 'FixedFormatter',
'NullFormatter', 'FuncFormatter', 'FormatStrFormatter',
'ScalarFormatter', 'LogFormatter', 'LogFormatterExponent',
Something went wrong with that request. Please try again.