From da9e5d5ec021727585226ad37c2c677fd045ec94 Mon Sep 17 00:00:00 2001 From: Ryan May Date: Thu, 17 Nov 2016 11:01:31 -0700 Subject: [PATCH 1/7] BUG: Fix drawing gridlines in skew-T example (Fixes #6873) For some reason, `_adjust_location()` on the `SkewSpine` was not being called; this resulted in `SkewXAxis` having the default (and out of date) bounds for the upper x-axis. Not sure what the root cause is, but refactoring this so that it isn't relying on the Spine to modify attributes on the XAxis both fixes the problem and cleans up the design greatly. --- examples/api/skewt.py | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/examples/api/skewt.py b/examples/api/skewt.py index f586fc388a4e..a6678cff8237 100644 --- a/examples/api/skewt.py +++ b/examples/api/skewt.py @@ -18,13 +18,11 @@ import matplotlib.transforms as transforms import matplotlib.axis as maxis import matplotlib.spines as mspines -import matplotlib.path as mpath from matplotlib.projections import register_projection + # The sole purpose of this class is to look at the upper, lower, or total # interval as appropriate and see what parts of the tick to draw, if any. - - class SkewXTick(maxis.XTick): def draw(self, renderer): if not self.get_visible(): @@ -56,10 +54,6 @@ def draw(self, renderer): # This class exists to provide two separate sets of intervals to the tick, # as well as create instances of the custom tick class SkewXAxis(maxis.XAxis): - def __init__(self, *args, **kwargs): - maxis.XAxis.__init__(self, *args, **kwargs) - self.upper_interval = 0.0, 1.0 - def _get_tick(self, major): return SkewXTick(self.axes, 0, '', major=major) @@ -67,8 +61,12 @@ def _get_tick(self, major): def lower_interval(self): return self.axes.viewLim.intervalx + @property + def upper_interval(self): + return self.axes.upper_xlim + def get_view_interval(self): - return self.upper_interval[0], self.axes.viewLim.intervalx[1] + return self.upper_interval[0], self.lower_interval[1] # This class exists to calculate the separate data range of the @@ -76,18 +74,11 @@ def get_view_interval(self): # to the X-axis artist for ticking and gridlines class SkewSpine(mspines.Spine): def _adjust_location(self): - trans = self.axes.transDataToAxes.inverted() + pts = self._path.vertices if self.spine_type == 'top': - yloc = 1.0 + pts[:, 0] = self.axis.upper_interval else: - yloc = 0.0 - left = trans.transform_point((0.0, yloc))[0] - right = trans.transform_point((1.0, yloc))[0] - - pts = self._path.vertices - pts[0, 0] = left - pts[1, 0] = right - self.axis.upper_interval = (left, right) + pts[:, 0] = self.axis.lower_interval # This class handles registration of the skew-xaxes as a projection as well @@ -143,6 +134,12 @@ def _set_lim_and_transforms(self): transforms.IdentityTransform()) + transforms.Affine2D().skew_deg(rot, 0)) + self.transAxes + @property + def upper_xlim(self): + pts = [[0., 1.], [1., 1.]] + return self.transDataToAxes.inverted().transform(pts)[:, 0] + + # Now register the projection with matplotlib so the user can select # it. register_projection(SkewXAxes) @@ -242,7 +239,7 @@ def _set_lim_and_transforms(self): plt.grid(True) # Plot the data using normal plotting functions, in this case using - # log scaling in Y, as dicatated by the typical meteorological plot + # log scaling in Y, as dictated by the typical meteorological plot ax.semilogy(T, p) ax.semilogy(Td, p) From 5c11d2c7b90a6f4b2895557688e652ff9d513d64 Mon Sep 17 00:00:00 2001 From: Ryan May Date: Thu, 17 Nov 2016 11:04:59 -0700 Subject: [PATCH 2/7] MNT: Explicitly disable minor ticks on Skew-T example With 2.0, we now get minor ticks on log plots that don't span a full decade (like this one if we go 1000 to 100). Just turn them off. --- examples/api/skewt.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/api/skewt.py b/examples/api/skewt.py index a6678cff8237..29dccd02a8e0 100644 --- a/examples/api/skewt.py +++ b/examples/api/skewt.py @@ -146,7 +146,8 @@ def upper_xlim(self): if __name__ == '__main__': # Now make a simple example using the custom projection. - from matplotlib.ticker import ScalarFormatter, MultipleLocator + from matplotlib.ticker import (MultipleLocator, NullFormatter, + ScalarFormatter) import matplotlib.pyplot as plt from six import StringIO import numpy as np @@ -248,6 +249,7 @@ def upper_xlim(self): # Disables the log-formatting that comes with semilogy ax.yaxis.set_major_formatter(ScalarFormatter()) + ax.yaxis.set_minor_formatter(NullFormatter()) ax.set_yticks(np.linspace(100, 1000, 10)) ax.set_ylim(1050, 100) From 749465519694dca46e8abcec744121edaeb83586 Mon Sep 17 00:00:00 2001 From: Ryan May Date: Thu, 17 Nov 2016 11:06:45 -0700 Subject: [PATCH 3/7] MNT: Update colors on Skew-T example Grab some other colors out of the default cycle so that temp is red, dewpoint is green, and the slanted line is blue. --- examples/api/skewt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/api/skewt.py b/examples/api/skewt.py index 29dccd02a8e0..d71f203bc22c 100644 --- a/examples/api/skewt.py +++ b/examples/api/skewt.py @@ -241,11 +241,11 @@ def upper_xlim(self): # Plot the data using normal plotting functions, in this case using # log scaling in Y, as dictated by the typical meteorological plot - ax.semilogy(T, p) - ax.semilogy(Td, p) + ax.semilogy(T, p, color='C3') + ax.semilogy(Td, p, color='C2') # An example of a slanted line at constant X - l = ax.axvline(0) + l = ax.axvline(0, color='C0') # Disables the log-formatting that comes with semilogy ax.yaxis.set_major_formatter(ScalarFormatter()) From 42d50978f7b3981505fa48f542aaec16bdccea96 Mon Sep 17 00:00:00 2001 From: Ryan May Date: Thu, 17 Nov 2016 11:08:01 -0700 Subject: [PATCH 4/7] MNT: Clean up skew test Make it match the refactoring on the example. --- lib/matplotlib/tests/test_skew.py | 51 ++++++++++++------------------- 1 file changed, 19 insertions(+), 32 deletions(-) diff --git a/lib/matplotlib/tests/test_skew.py b/lib/matplotlib/tests/test_skew.py index 662bbc6cbb8c..115d85c8d37c 100644 --- a/lib/matplotlib/tests/test_skew.py +++ b/lib/matplotlib/tests/test_skew.py @@ -5,18 +5,14 @@ unicode_literals) import itertools -import six -from nose.tools import assert_true -import numpy as np import matplotlib.pyplot as plt -from matplotlib.testing.decorators import cleanup, image_comparison +from matplotlib.testing.decorators import image_comparison from matplotlib.axes import Axes import matplotlib.transforms as transforms import matplotlib.axis as maxis import matplotlib.spines as mspines -import matplotlib.path as mpath import matplotlib.patches as mpatch from matplotlib.projections import register_projection @@ -54,10 +50,6 @@ def draw(self, renderer): # This class exists to provide two separate sets of intervals to the tick, # as well as create instances of the custom tick class SkewXAxis(maxis.XAxis): - def __init__(self, *args, **kwargs): - maxis.XAxis.__init__(self, *args, **kwargs) - self.upper_interval = 0.0, 1.0 - def _get_tick(self, major): return SkewXTick(self.axes, 0, '', major=major) @@ -65,35 +57,24 @@ def _get_tick(self, major): def lower_interval(self): return self.axes.viewLim.intervalx + @property + def upper_interval(self): + return self.axes.upper_xlim + def get_view_interval(self): - return self.upper_interval[0], self.axes.viewLim.intervalx[1] + return self.upper_interval[0], self.lower_interval[1] # This class exists to calculate the separate data range of the # upper X-axis and draw the spine there. It also provides this range # to the X-axis artist for ticking and gridlines class SkewSpine(mspines.Spine): - def __init__(self, axes, spine_type): - if spine_type == 'bottom': - loc = 0.0 - else: - loc = 1.0 - mspines.Spine.__init__(self, axes, spine_type, - mpath.Path([(13, loc), (13, loc)])) - def _adjust_location(self): - trans = self.axes.transDataToAxes.inverted() + pts = self._path.vertices if self.spine_type == 'top': - yloc = 1.0 + pts[:, 0] = self.axis.upper_interval else: - yloc = 0.0 - left = trans.transform_point((0.0, yloc))[0] - right = trans.transform_point((1.0, yloc))[0] - - pts = self._path.vertices - pts[0, 0] = left - pts[1, 0] = right - self.axis.upper_interval = (left, right) + pts[:, 0] = self.axis.lower_interval # This class handles registration of the skew-xaxes as a projection as well @@ -106,7 +87,7 @@ class SkewXAxes(Axes): name = 'skewx' def _init_axis(self): - #Taken from Axes and modified to use our modified X-axis + # Taken from Axes and modified to use our modified X-axis self.xaxis = SkewXAxis(self) self.spines['top'].register_axis(self.xaxis) self.spines['bottom'].register_axis(self.xaxis) @@ -115,7 +96,7 @@ def _init_axis(self): self.spines['right'].register_axis(self.yaxis) def _gen_axes_spines(self): - spines = {'top': SkewSpine(self, 'top'), + spines = {'top': SkewSpine.linear_spine(self, 'top'), 'bottom': mspines.Spine.linear_spine(self, 'bottom'), 'left': mspines.Spine.linear_spine(self, 'left'), 'right': mspines.Spine.linear_spine(self, 'right')} @@ -128,7 +109,7 @@ def _set_lim_and_transforms(self): """ rot = 30 - #Get the standard transform setup from the Axes base class + # Get the standard transform setup from the Axes base class Axes._set_lim_and_transforms(self) # Need to put the skew in the middle, after the scale and limits, @@ -150,6 +131,12 @@ def _set_lim_and_transforms(self): transforms.IdentityTransform()) + transforms.Affine2D().skew_deg(rot, 0)) + self.transAxes + @property + def upper_xlim(self): + pts = [[0., 1.], [1., 1.]] + return self.transDataToAxes.inverted().transform(pts)[:, 0] + + # Now register the projection with matplotlib so the user can select # it. register_projection(SkewXAxes) @@ -164,7 +151,7 @@ def test_set_line_coll_dash_image(): ax.grid(True) # An example of a slanted line at constant X - l = ax.axvline(0, color='b') + ax.axvline(0, color='b') @image_comparison(baseline_images=['skew_rects'], remove_text=True) From 6777e33605a83ea09beeb07ac8e04bb84e7fb6da Mon Sep 17 00:00:00 2001 From: Ryan May Date: Thu, 17 Nov 2016 22:34:53 -0700 Subject: [PATCH 5/7] BUG: Fix handling of ticks in Skew example. Refactor so that ticks, labels, and gridlines are controlled using their flags, compensating for data ranges, rather than a hard-coded check in the draw method. This fixes figures with a bunch of whitespace when using bbox_inches='tight'. Also add this refactor to the test. --- examples/api/skewt.py | 85 ++++++++++++++++++++----------- lib/matplotlib/tests/test_skew.py | 85 ++++++++++++++++++++----------- 2 files changed, 108 insertions(+), 62 deletions(-) diff --git a/examples/api/skewt.py b/examples/api/skewt.py index d71f203bc22c..2a2ce4fb18b6 100644 --- a/examples/api/skewt.py +++ b/examples/api/skewt.py @@ -24,31 +24,58 @@ # The sole purpose of this class is to look at the upper, lower, or total # interval as appropriate and see what parts of the tick to draw, if any. class SkewXTick(maxis.XTick): - def draw(self, renderer): - if not self.get_visible(): - return - renderer.open_group(self.__name__) + def _need_lower(self): + return transforms.interval_contains(self.axes.lower_xlim, + self.get_loc()) - lower_interval = self.axes.xaxis.lower_interval - upper_interval = self.axes.xaxis.upper_interval + def _need_upper(self): + return transforms.interval_contains(self.axes.upper_xlim, + self.get_loc()) - if self.gridOn and transforms.interval_contains( - self.axes.xaxis.get_view_interval(), self.get_loc()): - self.gridline.draw(renderer) + @property + def gridOn(self): + return (self._gridOn and + transforms.interval_contains(self.get_view_interval(), + self.get_loc())) + + @gridOn.setter + def gridOn(self, value): + self._gridOn = value + + @property + def tick1On(self): + return self._tick1On and self._need_lower() - if transforms.interval_contains(lower_interval, self.get_loc()): - if self.tick1On: - self.tick1line.draw(renderer) - if self.label1On: - self.label1.draw(renderer) + @tick1On.setter + def tick1On(self, value): + self._tick1On = value - if transforms.interval_contains(upper_interval, self.get_loc()): - if self.tick2On: - self.tick2line.draw(renderer) - if self.label2On: - self.label2.draw(renderer) + @property + def label1On(self): + return self._label1On and self._need_lower() - renderer.close_group(self.__name__) + @label1On.setter + def label1On(self, value): + self._label1On = value + + @property + def tick2On(self): + return self._tick2On and self._need_upper() + + @tick2On.setter + def tick2On(self, value): + self._tick2On = value + + @property + def label2On(self): + return self._label2On and self._need_upper() + + @label2On.setter + def label2On(self, value): + self._label2On = value + + def get_view_interval(self): + return self.axes.xaxis.get_view_interval() # This class exists to provide two separate sets of intervals to the tick, @@ -57,16 +84,8 @@ class SkewXAxis(maxis.XAxis): def _get_tick(self, major): return SkewXTick(self.axes, 0, '', major=major) - @property - def lower_interval(self): - return self.axes.viewLim.intervalx - - @property - def upper_interval(self): - return self.axes.upper_xlim - def get_view_interval(self): - return self.upper_interval[0], self.lower_interval[1] + return self.axes.upper_xlim[0], self.axes.lower_xlim[1] # This class exists to calculate the separate data range of the @@ -76,9 +95,9 @@ class SkewSpine(mspines.Spine): def _adjust_location(self): pts = self._path.vertices if self.spine_type == 'top': - pts[:, 0] = self.axis.upper_interval + pts[:, 0] = self.axes.upper_xlim else: - pts[:, 0] = self.axis.lower_interval + pts[:, 0] = self.axes.lower_xlim # This class handles registration of the skew-xaxes as a projection as well @@ -134,6 +153,10 @@ def _set_lim_and_transforms(self): transforms.IdentityTransform()) + transforms.Affine2D().skew_deg(rot, 0)) + self.transAxes + @property + def lower_xlim(self): + return self.axes.viewLim.intervalx + @property def upper_xlim(self): pts = [[0., 1.], [1., 1.]] diff --git a/lib/matplotlib/tests/test_skew.py b/lib/matplotlib/tests/test_skew.py index 115d85c8d37c..68bb17507db0 100644 --- a/lib/matplotlib/tests/test_skew.py +++ b/lib/matplotlib/tests/test_skew.py @@ -20,31 +20,58 @@ # The sole purpose of this class is to look at the upper, lower, or total # interval as appropriate and see what parts of the tick to draw, if any. class SkewXTick(maxis.XTick): - def draw(self, renderer): - if not self.get_visible(): - return - renderer.open_group(self.__name__) + def _need_lower(self): + return transforms.interval_contains(self.axes.lower_xlim, + self.get_loc()) - lower_interval = self.axes.xaxis.lower_interval - upper_interval = self.axes.xaxis.upper_interval + def _need_upper(self): + return transforms.interval_contains(self.axes.upper_xlim, + self.get_loc()) - if self.gridOn and transforms.interval_contains( - self.axes.xaxis.get_view_interval(), self.get_loc()): - self.gridline.draw(renderer) + @property + def gridOn(self): + return (self._gridOn and + transforms.interval_contains(self.get_view_interval(), + self.get_loc())) + + @gridOn.setter + def gridOn(self, value): + self._gridOn = value + + @property + def tick1On(self): + return self._tick1On and self._need_lower() - if transforms.interval_contains(lower_interval, self.get_loc()): - if self.tick1On: - self.tick1line.draw(renderer) - if self.label1On: - self.label1.draw(renderer) + @tick1On.setter + def tick1On(self, value): + self._tick1On = value - if transforms.interval_contains(upper_interval, self.get_loc()): - if self.tick2On: - self.tick2line.draw(renderer) - if self.label2On: - self.label2.draw(renderer) + @property + def label1On(self): + return self._label1On and self._need_lower() - renderer.close_group(self.__name__) + @label1On.setter + def label1On(self, value): + self._label1On = value + + @property + def tick2On(self): + return self._tick2On and self._need_upper() + + @tick2On.setter + def tick2On(self, value): + self._tick2On = value + + @property + def label2On(self): + return self._label2On and self._need_upper() + + @label2On.setter + def label2On(self, value): + self._label2On = value + + def get_view_interval(self): + return self.axes.xaxis.get_view_interval() # This class exists to provide two separate sets of intervals to the tick, @@ -53,16 +80,8 @@ class SkewXAxis(maxis.XAxis): def _get_tick(self, major): return SkewXTick(self.axes, 0, '', major=major) - @property - def lower_interval(self): - return self.axes.viewLim.intervalx - - @property - def upper_interval(self): - return self.axes.upper_xlim - def get_view_interval(self): - return self.upper_interval[0], self.lower_interval[1] + return self.axes.upper_xlim[0], self.axes.lower_xlim[1] # This class exists to calculate the separate data range of the @@ -72,9 +91,9 @@ class SkewSpine(mspines.Spine): def _adjust_location(self): pts = self._path.vertices if self.spine_type == 'top': - pts[:, 0] = self.axis.upper_interval + pts[:, 0] = self.axes.upper_xlim else: - pts[:, 0] = self.axis.lower_interval + pts[:, 0] = self.axes.lower_xlim # This class handles registration of the skew-xaxes as a projection as well @@ -131,6 +150,10 @@ def _set_lim_and_transforms(self): transforms.IdentityTransform()) + transforms.Affine2D().skew_deg(rot, 0)) + self.transAxes + @property + def lower_xlim(self): + return self.axes.viewLim.intervalx + @property def upper_xlim(self): pts = [[0., 1.], [1., 1.]] From 38183979e58f410d82e511d71eebd053c874944d Mon Sep 17 00:00:00 2001 From: Ryan May Date: Fri, 18 Nov 2016 12:14:34 -0700 Subject: [PATCH 6/7] MNT: Add empty _get_text?_transform to Axis. Such an implementation already exists for update_position(), add some for these methods for clarity and to help users who want to be able to follow them from where they're called (like in an IDE). --- lib/matplotlib/axis.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 1167f4161d25..cbae01d37bdb 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -344,6 +344,12 @@ def update_position(self, loc): 'Set the location of tick in data coords with scalar *loc*' raise NotImplementedError('Derived must override') + def _get_text1_transform(self): + raise NotImplementedError('Derived must override') + + def _get_text2_transform(self): + raise NotImplementedError('Derived must override') + class XTick(Tick): """ From 73ee9f26073a96ee982a2fccaf9bd9331e151835 Mon Sep 17 00:00:00 2001 From: Ryan May Date: Fri, 18 Nov 2016 12:16:52 -0700 Subject: [PATCH 7/7] BUG: Fix Skew tick implementation. Previous fixes broke upper ticks so that they never showed up. This comes from checking the default position, 0, against the upper range. Instead, use a sentinel of None for the default position. If a tick has the default position, avoid checking against the interval. --- examples/api/skewt.py | 25 ++++++++++++++++++------- lib/matplotlib/tests/test_skew.py | 25 ++++++++++++++++++------- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/examples/api/skewt.py b/examples/api/skewt.py index 2a2ce4fb18b6..93891f5a2122 100644 --- a/examples/api/skewt.py +++ b/examples/api/skewt.py @@ -24,19 +24,30 @@ # The sole purpose of this class is to look at the upper, lower, or total # interval as appropriate and see what parts of the tick to draw, if any. class SkewXTick(maxis.XTick): + def update_position(self, loc): + # This ensures that the new value of the location is set before + # any other updates take place + self._loc = loc + super(SkewXTick, self).update_position(loc) + + def _has_default_loc(self): + return self.get_loc() is None + def _need_lower(self): - return transforms.interval_contains(self.axes.lower_xlim, - self.get_loc()) + return (self._has_default_loc() or + transforms.interval_contains(self.axes.lower_xlim, + self.get_loc())) def _need_upper(self): - return transforms.interval_contains(self.axes.upper_xlim, - self.get_loc()) + return (self._has_default_loc() or + transforms.interval_contains(self.axes.upper_xlim, + self.get_loc())) @property def gridOn(self): - return (self._gridOn and + return (self._gridOn and (self._has_default_loc() or transforms.interval_contains(self.get_view_interval(), - self.get_loc())) + self.get_loc()))) @gridOn.setter def gridOn(self, value): @@ -82,7 +93,7 @@ def get_view_interval(self): # as well as create instances of the custom tick class SkewXAxis(maxis.XAxis): def _get_tick(self, major): - return SkewXTick(self.axes, 0, '', major=major) + return SkewXTick(self.axes, None, '', major=major) def get_view_interval(self): return self.axes.upper_xlim[0], self.axes.lower_xlim[1] diff --git a/lib/matplotlib/tests/test_skew.py b/lib/matplotlib/tests/test_skew.py index 68bb17507db0..27a175b3771d 100644 --- a/lib/matplotlib/tests/test_skew.py +++ b/lib/matplotlib/tests/test_skew.py @@ -20,19 +20,30 @@ # The sole purpose of this class is to look at the upper, lower, or total # interval as appropriate and see what parts of the tick to draw, if any. class SkewXTick(maxis.XTick): + def update_position(self, loc): + # This ensures that the new value of the location is set before + # any other updates take place + self._loc = loc + super(SkewXTick, self).update_position(loc) + + def _has_default_loc(self): + return self.get_loc() is None + def _need_lower(self): - return transforms.interval_contains(self.axes.lower_xlim, - self.get_loc()) + return (self._has_default_loc() or + transforms.interval_contains(self.axes.lower_xlim, + self.get_loc())) def _need_upper(self): - return transforms.interval_contains(self.axes.upper_xlim, - self.get_loc()) + return (self._has_default_loc() or + transforms.interval_contains(self.axes.upper_xlim, + self.get_loc())) @property def gridOn(self): - return (self._gridOn and + return (self._gridOn and (self._has_default_loc() or transforms.interval_contains(self.get_view_interval(), - self.get_loc())) + self.get_loc()))) @gridOn.setter def gridOn(self, value): @@ -78,7 +89,7 @@ def get_view_interval(self): # as well as create instances of the custom tick class SkewXAxis(maxis.XAxis): def _get_tick(self, major): - return SkewXTick(self.axes, 0, '', major=major) + return SkewXTick(self.axes, None, '', major=major) def get_view_interval(self): return self.axes.upper_xlim[0], self.axes.lower_xlim[1]