Skip to content

Specify custom tick space heuristic in MaxNLocator #11027

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

Closed
wants to merge 1 commit into from
Closed
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
23 changes: 15 additions & 8 deletions lib/matplotlib/axis.py
Original file line number Diff line number Diff line change
Expand Up @@ -1760,6 +1760,11 @@ def get_minpos(self):
raise NotImplementedError()


def tick_space_heuristic(axis_length, label_fontsize, label_fontsize_factor):
Copy link
Member

Choose a reason for hiding this comment

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

This should be private I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You mean prepend the function with an underscore?

Copy link
Member

Choose a reason for hiding this comment

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

Yes.

size = label_fontsize * label_fontsize_factor
return int(np.floor(axis_length / size))


class XAxis(Axis):
__name__ = 'xaxis'
axis_name = 'x'
Expand Down Expand Up @@ -2117,15 +2122,16 @@ def set_default_intervals(self):
self.axes.viewLim.intervalx = xmin, xmax
self.stale = True

def get_tick_space(self):
def get_tick_space(self,
heuristic=lambda x, y: tick_space_heuristic(x, y, 3)):
ends = self.axes.transAxes.transform([[0, 0], [1, 0]])
length = ((ends[1][0] - ends[0][0]) / self.axes.figure.dpi) * 72
tick = self._get_tick(True)
# There is a heuristic here that the aspect ratio of tick text
# is no more than 3:1
size = tick.label1.get_size() * 3
if size > 0:
return int(np.floor(length / size))
label_fontsize = tick.label1.get_size()
if label_fontsize > 0:
return heuristic(length, label_fontsize)
else:
return 2**31 - 1

Expand Down Expand Up @@ -2496,13 +2502,14 @@ def set_default_intervals(self):
self.axes.viewLim.intervaly = ymin, ymax
self.stale = True

def get_tick_space(self):
def get_tick_space(self,
heuristic=lambda x, y: tick_space_heuristic(x, y, 2.0)):
ends = self.axes.transAxes.transform([[0, 0], [0, 1]])
length = ((ends[1][1] - ends[0][1]) / self.axes.figure.dpi) * 72
tick = self._get_tick(True)
# Having a spacing of at least 2 just looks good.
size = tick.label1.get_size() * 2.0
if size > 0:
return int(np.floor(length / size))
label_fontsize = tick.label1.get_size()
if label_fontsize > 0:
return heuristic(length, label_fontsize)
else:
return 2**31 - 1
15 changes: 15 additions & 0 deletions lib/matplotlib/tests/test_ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import matplotlib.axis as maxis


class TestMaxNLocator(object):
Expand All @@ -22,6 +23,13 @@ class TestMaxNLocator(object):
(1, 55, [1, 1.5, 5, 6, 10], np.array([0, 15, 30, 45, 60])),
]

heuristic_data = [
(-0.5, 9.5, None, np.array([-5., 0., 5., 10.])),
(-0.5, 9.5,
lambda x, y: maxis.tick_space_heuristic(x, y, 2.),
np.array([-4., 0., 4., 8., 12.]))
]

@pytest.mark.parametrize('vmin, vmax, expected', basic_data)
def test_basic(self, vmin, vmax, expected):
loc = mticker.MaxNLocator(nbins=5)
Expand All @@ -32,6 +40,13 @@ def test_integer(self, vmin, vmax, steps, expected):
loc = mticker.MaxNLocator(nbins=5, integer=True, steps=steps)
assert_almost_equal(loc.tick_values(vmin, vmax), expected)

@pytest.mark.parametrize('vmin, vmax, heuristic, expected', heuristic_data)
def test_tick_space(self, vmin, vmax, heuristic, expected):
fig, ax = plt.subplots(figsize=(2, 2))
loc = mticker.MaxNLocator(nbins='auto', tick_space_heuristic=heuristic)
loc.set_axis(ax.xaxis)
assert_almost_equal(loc.tick_values(vmin, vmax), expected)


class TestLinearLocator(object):
def test_basic(self):
Expand Down
17 changes: 15 additions & 2 deletions lib/matplotlib/ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1779,7 +1779,8 @@ class MaxNLocator(Locator):
integer=False,
symmetric=False,
prune=None,
min_n_ticks=2)
min_n_ticks=2,
tick_space_heuristic=None)

def __init__(self, *args, **kwargs):
"""
Expand Down Expand Up @@ -1821,6 +1822,12 @@ def __init__(self, *args, **kwargs):
Relax `nbins` and `integer` constraints if necessary to
obtain this minimum number of ticks.

*tick_space_heuristic*
Copy link
Member

Choose a reason for hiding this comment

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

I'd suggest that this just be another option for nbins. i.e. if nbins is callable it is used to determine the heuristic...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a great idea!

Controls spacing of ticks through `axis.get_tick_space`. This must
be a callable object that takes two parameters, the axis length and
the label font size, both in pt units. Default value `None` will
use the default x- or y-axis heuristic.

"""
if args:
kwargs['nbins'] = args[0]
Expand Down Expand Up @@ -1882,11 +1889,17 @@ def set_params(self, **kwargs):
self._extended_steps = self._staircase(self._steps)
if 'integer' in kwargs:
self._integer = kwargs['integer']
if 'tick_space_heuristic' in kwargs:
tick_space_heuristic = kwargs['tick_space_heuristic']
Copy link
Member

Choose a reason for hiding this comment

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

I think we just use kwarg.pop these days...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's fine by me. I was just following the style used in the rest of the set_params function. Do you think I should convert the entire function then? Seems like something for a different PR, but I don't mind doing it here.

if tick_space_heuristic is not None:
self._tickspace_kw = {'heuristic': tick_space_heuristic}
else:
self._tickspace_kw = {}

def _raw_ticks(self, vmin, vmax):
if self._nbins == 'auto':
if self.axis is not None:
nbins = np.clip(self.axis.get_tick_space(),
nbins = np.clip(self.axis.get_tick_space(**self._tickspace_kw),
max(1, self._min_n_ticks - 1), 9)
else:
nbins = 9
Expand Down