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

Skip to content

Fix lost ticks #1686

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 12 commits into from
Feb 19, 2013
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
78 changes: 77 additions & 1 deletion lib/matplotlib/axis.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import matplotlib.transforms as mtransforms
import matplotlib.units as munits
import numpy as np
import warnings

GRIDLINE_INTERPOLATION_STEPS = 180

Expand Down Expand Up @@ -972,11 +973,36 @@ def _update_ticks(self, renderer):
tick_tups = [ti for ti in tick_tups
if (ti[1] >= ilow) and (ti[1] <= ihigh)]

# so that we don't lose ticks on the end, expand out the interval ever so slightly. The
# "ever so slightly" is defined to be the width of a half of a pixel. We don't want to draw
# a tick that even one pixel outside of the defined axis interval.
if interval[0] <= interval[1]:
interval_expanded = interval
else:
interval_expanded = interval[1], interval[0]

if hasattr(self, '_get_pixel_distance_along_axis'):
# normally, one does not want to catch all exceptions that could possibly happen, but it
# is not clear exactly what exceptions might arise from a user's projection (their rendition
# of the Axis object). So, we catch all, with the idea that one would rather potentially
# lose a tick from one side of the axis or another, rather than see a stack trace.
try:
ds1 = self._get_pixel_distance_along_axis(interval_expanded[0], -0.5)
except:
warnings.warn("Unable to find pixel distance along axis for interval padding; assuming no interval padding needed.")
ds1 = 0.0
try:
ds2 = self._get_pixel_distance_along_axis(interval_expanded[1], +0.5)
except:
warnings.warn("Unable to find pixel distance along axis for interval padding; assuming no interval padding needed.")
ds2 = 0.0
interval_expanded = (interval[0] - ds1, interval[1] + ds2)

ticks_to_draw = []
for tick, loc, label in tick_tups:
if tick is None:
continue
if not mtransforms.interval_contains(interval, loc):
if not mtransforms.interval_contains(interval_expanded, loc):
continue
tick.update_position(loc)
tick.set_label1(label)
Expand Down Expand Up @@ -1599,6 +1625,35 @@ def _get_offset_text(self):
self.offset_text_position = 'bottom'
return offsetText

def _get_pixel_distance_along_axis(self, where, perturb):
"""
Returns the amount, in data coordinates, that a single pixel corresponds to in the
locality given by "where", which is also given in data coordinates, and is an x coordinate.
"perturb" is the amount to perturb the pixel. Usually +0.5 or -0.5.

Implementing this routine for an axis is optional; if present, it will ensure that no
ticks are lost due to round-off at the extreme ends of an axis.
"""

# Note that this routine does not work for a polar axis, because of the 1e-10 below. To
# do things correctly, we need to use rmax instead of 1e-10 for a polar axis. But
# since we do not have that kind of information at this point, we just don't try to
# pad anything for the theta axis of a polar plot.
if self.axes.name == 'polar':
Copy link
Member

Choose a reason for hiding this comment

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

I don't really like this approach. What if I wanted to implement my own projection and it didn't even have an inverted data transformation? The safer, though perhaps not best, option would be to make sure self.axes were rectangular (I'd be happy with that).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm; I think the approach is correct here...the only problem with the polar projection is the singularity at r=0, which is why it is specifically exempted. The code should activate for everything else.

As for not having an inverted transformation, would that even make sense? Not being able to go from pixel coordinates to data coordinates? Seems like there would be quite a few other things that would break in that situation; not just this.

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 @pelson's point might be that it's not a great idea to single out a particular projection here -- the user can install and use any number of projections. I haven't given it much thought, but it would be great to find a way to detect this case other than just checking for polar.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The singling out, though, is for the one projection that I know it doesn't work for. It should work for others, but just for safety, in the latest commits, I wrapped a try/except around each call that gets the amount of padding to use, which defaults the padding to zero in the case of a problem. This recovers old matplotlib behavior.

return 0.0

#
# first figure out the pixel location of the "where" point. We use 1e-10 for the
# y point, so that we remain compatible with log axes.
#
trans = self.axes.transData # transformation from data coords to display coords
transinv = trans.inverted() # transformation from display coords to data coords
pix = trans.transform_point((where, 1e-10))
ptp = transinv.transform_point((pix[0] + perturb, pix[1])) # perturb the pixel.
dx = abs(ptp[0] - where)

return dx

def get_label_position(self):
"""
Return the label position (top or bottom)
Expand Down Expand Up @@ -1874,6 +1929,27 @@ def _get_offset_text(self):
self.offset_text_position = 'left'
return offsetText

def _get_pixel_distance_along_axis(self, where, perturb):
"""
Returns the amount, in data coordinates, that a single pixel corresponds to in the
locality given by "where", which is also given in data coordinates, and is an y coordinate.
"perturb" is the amount to perturb the pixel. Usually +0.5 or -0.5.

Implementing this routine for an axis is optional; if present, it will ensure that no
ticks are lost due to round-off at the extreme ends of an axis.
"""

#
# first figure out the pixel location of the "where" point. We use 1e-10 for the
# x point, so that we remain compatible with log axes.
#
trans = self.axes.transData # transformation from data coords to display coords
transinv = trans.inverted() # transformation from display coords to data coords
pix = trans.transform_point((1e-10, where))
ptp = transinv.transform_point((pix[0], pix[1] + perturb)) # perturb the pixel.
dy = abs(ptp[1] - where)
return dy

def get_label_position(self):
"""
Return the label position (left or right)
Expand Down
8 changes: 4 additions & 4 deletions lib/matplotlib/tests/test_ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def test_LinearLocator():

def test_MultipleLocator():
loc = mticker.MultipleLocator(base=3.147)
test_value = np.array([-6.294, -3.147, 0., 3.147, 6.294, 9.441])
test_value = np.array([-9.441, -6.294, -3.147, 0., 3.147, 6.294, 9.441, 12.588])
assert_almost_equal(loc.tick_values(-7, 10), test_value)


Expand All @@ -35,12 +35,12 @@ def test_LogLocator():

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])
test_value = np.array([1.00000000e-05, 1.00000000e-03, 1.00000000e-01, 1.00000000e+01,
1.00000000e+03, 1.00000000e+05, 1.00000000e+07, 1.000000000e+09])
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.])
test_value = np.array([0.5, 1., 2., 4., 8., 16., 32., 64., 128., 256.])
assert_almost_equal(loc.tick_values(1, 100), test_value)


Expand Down
6 changes: 3 additions & 3 deletions lib/matplotlib/ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1165,7 +1165,7 @@ def tick_values(self, vmin, vmax):
vmin = self._base.ge(vmin)
base = self._base.get_base()
n = (vmax - vmin + 0.001 * base) // base
locs = vmin + np.arange(n + 1) * base
locs = vmin - base + np.arange(n + 3) * base
return self.raise_if_exceeds(locs)

def view_limits(self, dmin, dmax):
Expand Down Expand Up @@ -1450,8 +1450,8 @@ def tick_values(self, vmin, vmax):
while numdec / stride + 1 > self.numticks:
stride += 1

decades = np.arange(math.floor(vmin),
math.ceil(vmax) + stride, stride)
decades = np.arange(math.floor(vmin) - stride,
math.ceil(vmax) + 2 * stride, stride)
if hasattr(self, '_transform'):
ticklocs = self._transform.inverted().transform(decades)
if len(subs) > 1 or (len(subs == 1) and subs[0] != 1.0):
Expand Down