""" Testing that skewed axes properly work """ from __future__ import (absolute_import, division, print_function, 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.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 # 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__) lower_interval = self.axes.xaxis.lower_interval upper_interval = self.axes.xaxis.upper_interval if self.gridOn and transforms.interval_contains( self.axes.xaxis.get_view_interval(), self.get_loc()): self.gridline.draw(renderer) if transforms.interval_contains(lower_interval, self.get_loc()): if self.tick1On: self.tick1line.draw(renderer) if self.label1On: self.label1.draw(renderer) if transforms.interval_contains(upper_interval, self.get_loc()): if self.tick2On: self.tick2line.draw(renderer) if self.label2On: self.label2.draw(renderer) renderer.close_group(self.__name__) # 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) @property def lower_interval(self): return self.axes.viewLim.intervalx def get_view_interval(self): return self.upper_interval[0], self.axes.viewLim.intervalx[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() if self.spine_type == 'top': yloc = 1.0 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) # This class handles registration of the skew-xaxes as a projection as well # as setting up the appropriate transformations. It also overrides standard # spines and axes instances as appropriate. class SkewXAxes(Axes): # The projection must specify a name. This will be used be the # user to select the projection, i.e. ``subplot(111, # projection='skewx')``. name = 'skewx' def _init_axis(self): #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) self.yaxis = maxis.YAxis(self) self.spines['left'].register_axis(self.yaxis) self.spines['right'].register_axis(self.yaxis) def _gen_axes_spines(self): spines = {'top': SkewSpine(self, 'top'), 'bottom': mspines.Spine.linear_spine(self, 'bottom'), 'left': mspines.Spine.linear_spine(self, 'left'), 'right': mspines.Spine.linear_spine(self, 'right')} return spines def _set_lim_and_transforms(self): """ This is called once when the plot is created to set up all the transforms for the data, text and grids. """ rot = 30 #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, # but before the transAxes. This way, the skew is done in Axes # coordinates thus performing the transform around the proper origin # We keep the pre-transAxes transform around for other users, like the # spines for finding bounds self.transDataToAxes = (self.transScale + (self.transLimits + transforms.Affine2D().skew_deg(rot, 0))) # Create the full transform from Data to Pixels self.transData = self.transDataToAxes + self.transAxes # Blended transforms like this need to have the skewing applied using # both axes, in axes coords like before. self._xaxis_transform = (transforms.blended_transform_factory( self.transScale + self.transLimits, transforms.IdentityTransform()) + transforms.Affine2D().skew_deg(rot, 0)) + self.transAxes # Now register the projection with matplotlib so the user can select # it. register_projection(SkewXAxes) @image_comparison(baseline_images=['skew_axes'], remove_text=True) def test_set_line_coll_dash_image(): fig = plt.figure() ax = fig.add_subplot(1, 1, 1, projection='skewx') ax.set_xlim(-50, 50) ax.set_ylim(50, -50) ax.grid(True) # An example of a slanted line at constant X l = ax.axvline(0, color='b') @image_comparison(baseline_images=['skew_rects'], remove_text=True) def test_skew_rectange(): fix, axes = plt.subplots(5, 5, sharex=True, sharey=True, figsize=(16, 12)) axes = axes.flat rotations = list(itertools.product([-3, -1, 0, 1, 3], repeat=2)) axes[0].set_xlim([-4, 4]) axes[0].set_ylim([-4, 4]) axes[0].set_aspect('equal') for ax, (xrots, yrots) in zip(axes, rotations): xdeg, ydeg = 45 * xrots, 45 * yrots t = transforms.Affine2D().skew_deg(xdeg, ydeg) ax.set_title('Skew of {0} in X and {1} in Y'.format(xdeg, ydeg)) ax.add_patch(mpatch.Rectangle([-1, -1], 2, 2, transform=t + ax.transData, alpha=0.5, facecolor='coral')) plt.subplots_adjust(wspace=0, left=0, right=1, bottom=0) if __name__ == '__main__': import nose nose.runmodule(argv=['-s', '--with-doctest'], exit=False)