diff options
author | shumkovnd <shumkovnd@yandex-team.com> | 2023-11-10 14:39:34 +0300 |
---|---|---|
committer | shumkovnd <shumkovnd@yandex-team.com> | 2023-11-10 16:42:24 +0300 |
commit | 77eb2d3fdcec5c978c64e025ced2764c57c00285 (patch) | |
tree | c51edb0748ca8d4a08d7c7323312c27ba1a8b79a /contrib/python/matplotlib/py2/mpl_toolkits | |
parent | dd6d20cadb65582270ac23f4b3b14ae189704b9d (diff) | |
download | ydb-77eb2d3fdcec5c978c64e025ced2764c57c00285.tar.gz |
KIKIMR-19287: add task_stats_drawing script
Diffstat (limited to 'contrib/python/matplotlib/py2/mpl_toolkits')
47 files changed, 13928 insertions, 0 deletions
diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/__init__.py b/contrib/python/matplotlib/py2/mpl_toolkits/__init__.py new file mode 100644 index 0000000000..8d9942e652 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/__init__.py @@ -0,0 +1,4 @@ +try: + __import__('pkg_resources').declare_namespace(__name__) +except ImportError: + pass # must not have setuptools diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/ChangeLog b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/ChangeLog new file mode 100644 index 0000000000..79cc01cfdf --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/ChangeLog @@ -0,0 +1,13 @@ +2009-06-01 Jae-Joon Lee <lee.j.joon@gmail.com> + + * axislines.py (Axes.toggle_axisline): fix broken spine support. + (AxisArtistHelper): Initial support for curvelinear grid and ticks. + +2009-05-04 Jae-Joon Lee <lee.j.joon@gmail.com> + + * inset_locator.py (inset_axes, zoomed_inset_axes): axes_class support + + * axislines.py : Better support for tick (tick label) color + handling + (Axes.get_children): fix typo + diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/__init__.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/__init__.py new file mode 100644 index 0000000000..c10e89bd62 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/__init__.py @@ -0,0 +1,15 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +from . import axes_size as Size +from .axes_divider import Divider, SubplotDivider, LocatableAxes, \ + make_axes_locatable +from .axes_grid import Grid, ImageGrid, AxesGrid +#from axes_divider import make_axes_locatable +from matplotlib.cbook import warn_deprecated +warn_deprecated(since='2.1', + name='mpl_toolkits.axes_grid', + alternative='mpl_toolkits.axes_grid1 and' + ' mpl_toolkits.axisartist provies the same' + ' functionality', + obj_type='module') diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/anchored_artists.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/anchored_artists.py new file mode 100644 index 0000000000..14b661497d --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/anchored_artists.py @@ -0,0 +1,9 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +from matplotlib.offsetbox import AnchoredOffsetbox, AuxTransformBox, VPacker,\ + TextArea, AnchoredText, DrawingArea, AnnotationBbox + +from mpl_toolkits.axes_grid1.anchored_artists import \ + AnchoredDrawingArea, AnchoredAuxTransformBox, \ + AnchoredEllipse, AnchoredSizeBar diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/angle_helper.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/angle_helper.py new file mode 100644 index 0000000000..f0f877d913 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/angle_helper.py @@ -0,0 +1,4 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +from mpl_toolkits.axisartist.angle_helper import * diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/axes_divider.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/axes_divider.py new file mode 100644 index 0000000000..25694ecf5e --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/axes_divider.py @@ -0,0 +1,8 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +from mpl_toolkits.axes_grid1.axes_divider import Divider, AxesLocator, SubplotDivider, \ + AxesDivider, locatable_axes_factory, make_axes_locatable + +from mpl_toolkits.axes_grid.axislines import Axes +LocatableAxes = locatable_axes_factory(Axes) diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/axes_grid.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/axes_grid.py new file mode 100644 index 0000000000..58212ac89c --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/axes_grid.py @@ -0,0 +1,30 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import mpl_toolkits.axes_grid1.axes_grid as axes_grid_orig +from .axes_divider import LocatableAxes + +class CbarAxes(axes_grid_orig.CbarAxesBase, LocatableAxes): + def __init__(self, *kl, **kwargs): + orientation=kwargs.pop("orientation", None) + if orientation is None: + raise ValueError("orientation must be specified") + self.orientation = orientation + self._default_label_on = False + self.locator = None + + super(LocatableAxes, self).__init__(*kl, **kwargs) + + def cla(self): + super(LocatableAxes, self).cla() + self._config_axes() + + +class Grid(axes_grid_orig.Grid): + _defaultLocatableAxesClass = LocatableAxes + +class ImageGrid(axes_grid_orig.ImageGrid): + _defaultLocatableAxesClass = LocatableAxes + _defaultCbarAxesClass = CbarAxes + +AxesGrid = ImageGrid diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/axes_rgb.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/axes_rgb.py new file mode 100644 index 0000000000..bfd4bb98ad --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/axes_rgb.py @@ -0,0 +1,11 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +#from mpl_toolkits.axes_grid1.axes_rgb import * +from mpl_toolkits.axes_grid1.axes_rgb import make_rgb_axes, imshow_rgb, RGBAxesBase + +#import mpl_toolkits.axes_grid1.axes_rgb as axes_rgb_orig +from .axislines import Axes + +class RGBAxes(RGBAxesBase): + _defaultAxesClass = Axes diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/axes_size.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/axes_size.py new file mode 100644 index 0000000000..998b5e3c87 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/axes_size.py @@ -0,0 +1,4 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +from mpl_toolkits.axes_grid1.axes_size import * diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/axis_artist.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/axis_artist.py new file mode 100644 index 0000000000..92f0538ceb --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/axis_artist.py @@ -0,0 +1,4 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +from mpl_toolkits.axisartist.axis_artist import * diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/axisline_style.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/axisline_style.py new file mode 100644 index 0000000000..2eef3b8b34 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/axisline_style.py @@ -0,0 +1,4 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +from mpl_toolkits.axisartist.axisline_style import * diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/axislines.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/axislines.py new file mode 100644 index 0000000000..9653aa1702 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/axislines.py @@ -0,0 +1,4 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +from mpl_toolkits.axisartist.axislines import * diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/clip_path.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/clip_path.py new file mode 100644 index 0000000000..bafe568fb1 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/clip_path.py @@ -0,0 +1,4 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +from mpl_toolkits.axisartist.clip_path import * diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/colorbar.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/colorbar.py new file mode 100644 index 0000000000..cc5c252da8 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/colorbar.py @@ -0,0 +1,5 @@ +from mpl_toolkits.axes_grid1.colorbar import ( + make_axes_kw_doc, colormap_kw_doc, colorbar_doc, + CbarAxesLocator, ColorbarBase, Colorbar, + make_axes, colorbar +) diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/floating_axes.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/floating_axes.py new file mode 100644 index 0000000000..3f30d57c3a --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/floating_axes.py @@ -0,0 +1,4 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +from mpl_toolkits.axisartist.floating_axes import * diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/grid_finder.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/grid_finder.py new file mode 100644 index 0000000000..ffa3db76cf --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/grid_finder.py @@ -0,0 +1,4 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +from mpl_toolkits.axisartist.grid_finder import * diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/grid_helper_curvelinear.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/grid_helper_curvelinear.py new file mode 100644 index 0000000000..325ddd6af2 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/grid_helper_curvelinear.py @@ -0,0 +1,4 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +from mpl_toolkits.axisartist.grid_helper_curvelinear import * diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/inset_locator.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/inset_locator.py new file mode 100644 index 0000000000..a9ed77beda --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/inset_locator.py @@ -0,0 +1,7 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +from mpl_toolkits.axes_grid1.inset_locator import InsetPosition, \ + AnchoredSizeLocator, \ + AnchoredZoomLocator, BboxPatch, BboxConnector, BboxConnectorPatch, \ + inset_axes, zoomed_inset_axes, mark_inset diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/parasite_axes.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/parasite_axes.py new file mode 100644 index 0000000000..cad56e43a2 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid/parasite_axes.py @@ -0,0 +1,18 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +from mpl_toolkits.axes_grid1.parasite_axes import ( + host_axes_class_factory, parasite_axes_class_factory, + parasite_axes_auxtrans_class_factory, subplot_class_factory) + +from .axislines import Axes + + +ParasiteAxes = parasite_axes_class_factory(Axes) + +ParasiteAxesAuxTrans = \ + parasite_axes_auxtrans_class_factory(axes_class=ParasiteAxes) + +HostAxes = host_axes_class_factory(axes_class=Axes) + +SubplotHost = subplot_class_factory(HostAxes) diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/__init__.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/__init__.py new file mode 100644 index 0000000000..3e225ba9f0 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/__init__.py @@ -0,0 +1,12 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six + +from . import axes_size as Size +from .axes_divider import Divider, SubplotDivider, LocatableAxes, \ + make_axes_locatable +from .axes_grid import Grid, ImageGrid, AxesGrid +#from axes_divider import make_axes_locatable + +from .parasite_axes import host_subplot, host_axes diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/anchored_artists.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/anchored_artists.py new file mode 100644 index 0000000000..5b492858e8 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/anchored_artists.py @@ -0,0 +1,376 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) +import six + +from matplotlib import docstring +from matplotlib.offsetbox import (AnchoredOffsetbox, AuxTransformBox, + DrawingArea, TextArea, VPacker) +from matplotlib.patches import Rectangle, Ellipse + + +__all__ = ['AnchoredDrawingArea', 'AnchoredAuxTransformBox', + 'AnchoredEllipse', 'AnchoredSizeBar'] + + +class AnchoredDrawingArea(AnchoredOffsetbox): + @docstring.dedent + def __init__(self, width, height, xdescent, ydescent, + loc, pad=0.4, borderpad=0.5, prop=None, frameon=True, + **kwargs): + """ + An anchored container with a fixed size and fillable DrawingArea. + + Artists added to the *drawing_area* will have their coordinates + interpreted as pixels. Any transformations set on the artists will be + overridden. + + Parameters + ---------- + width, height : int or float + width and height of the container, in pixels. + + xdescent, ydescent : int or float + descent of the container in the x- and y- direction, in pixels. + + loc : int + Location of this artist. Valid location codes are:: + + 'upper right' : 1, + 'upper left' : 2, + 'lower left' : 3, + 'lower right' : 4, + 'right' : 5, + 'center left' : 6, + 'center right' : 7, + 'lower center' : 8, + 'upper center' : 9, + 'center' : 10 + + pad : int or float, optional + Padding around the child objects, in fraction of the font + size. Defaults to 0.4. + + borderpad : int or float, optional + Border padding, in fraction of the font size. + Defaults to 0.5. + + prop : `matplotlib.font_manager.FontProperties`, optional + Font property used as a reference for paddings. + + frameon : bool, optional + If True, draw a box around this artists. Defaults to True. + + **kwargs : + Keyworded arguments to pass to + :class:`matplotlib.offsetbox.AnchoredOffsetbox`. + + Attributes + ---------- + drawing_area : `matplotlib.offsetbox.DrawingArea` + A container for artists to display. + + Examples + -------- + To display blue and red circles of different sizes in the upper right + of an axes *ax*: + + >>> ada = AnchoredDrawingArea(20, 20, 0, 0, loc=1, frameon=False) + >>> ada.drawing_area.add_artist(Circle((10, 10), 10, fc="b")) + >>> ada.drawing_area.add_artist(Circle((30, 10), 5, fc="r")) + >>> ax.add_artist(ada) + """ + self.da = DrawingArea(width, height, xdescent, ydescent) + self.drawing_area = self.da + + super(AnchoredDrawingArea, self).__init__( + loc, pad=pad, borderpad=borderpad, child=self.da, prop=None, + frameon=frameon, **kwargs + ) + + +class AnchoredAuxTransformBox(AnchoredOffsetbox): + @docstring.dedent + def __init__(self, transform, loc, + pad=0.4, borderpad=0.5, prop=None, frameon=True, **kwargs): + """ + An anchored container with transformed coordinates. + + Artists added to the *drawing_area* are scaled according to the + coordinates of the transformation used. The dimensions of this artist + will scale to contain the artists added. + + Parameters + ---------- + transform : `matplotlib.transforms.Transform` + The transformation object for the coordinate system in use, i.e., + :attr:`matplotlib.axes.Axes.transData`. + + loc : int + Location of this artist. Valid location codes are:: + + 'upper right' : 1, + 'upper left' : 2, + 'lower left' : 3, + 'lower right' : 4, + 'right' : 5, + 'center left' : 6, + 'center right' : 7, + 'lower center' : 8, + 'upper center' : 9, + 'center' : 10 + + pad : int or float, optional + Padding around the child objects, in fraction of the font + size. Defaults to 0.4. + + borderpad : int or float, optional + Border padding, in fraction of the font size. + Defaults to 0.5. + + prop : `matplotlib.font_manager.FontProperties`, optional + Font property used as a reference for paddings. + + frameon : bool, optional + If True, draw a box around this artists. Defaults to True. + + **kwargs : + Keyworded arguments to pass to + :class:`matplotlib.offsetbox.AnchoredOffsetbox`. + + Attributes + ---------- + drawing_area : `matplotlib.offsetbox.AuxTransformBox` + A container for artists to display. + + Examples + -------- + To display an ellipse in the upper left, with a width of 0.1 and + height of 0.4 in data coordinates: + + >>> box = AnchoredAuxTransformBox(ax.transData, loc=2) + >>> el = Ellipse((0,0), width=0.1, height=0.4, angle=30) + >>> box.drawing_area.add_artist(el) + >>> ax.add_artist(box) + """ + self.drawing_area = AuxTransformBox(transform) + + AnchoredOffsetbox.__init__(self, loc, pad=pad, borderpad=borderpad, + child=self.drawing_area, + prop=prop, + frameon=frameon, + **kwargs) + + +class AnchoredEllipse(AnchoredOffsetbox): + @docstring.dedent + def __init__(self, transform, width, height, angle, loc, + pad=0.1, borderpad=0.1, prop=None, frameon=True, **kwargs): + """ + Draw an anchored ellipse of a given size. + + Parameters + ---------- + transform : `matplotlib.transforms.Transform` + The transformation object for the coordinate system in use, i.e., + :attr:`matplotlib.axes.Axes.transData`. + + width, height : int or float + Width and height of the ellipse, given in coordinates of + *transform*. + + angle : int or float + Rotation of the ellipse, in degrees, anti-clockwise. + + loc : int + Location of this size bar. Valid location codes are:: + + 'upper right' : 1, + 'upper left' : 2, + 'lower left' : 3, + 'lower right' : 4, + 'right' : 5, + 'center left' : 6, + 'center right' : 7, + 'lower center' : 8, + 'upper center' : 9, + 'center' : 10 + + pad : int or float, optional + Padding around the ellipse, in fraction of the font size. Defaults + to 0.1. + + borderpad : int or float, optional + Border padding, in fraction of the font size. Defaults to 0.1. + + frameon : bool, optional + If True, draw a box around the ellipse. Defaults to True. + + prop : `matplotlib.font_manager.FontProperties`, optional + Font property used as a reference for paddings. + + **kwargs : + Keyworded arguments to pass to + :class:`matplotlib.offsetbox.AnchoredOffsetbox`. + + Attributes + ---------- + ellipse : `matplotlib.patches.Ellipse` + Ellipse patch drawn. + """ + self._box = AuxTransformBox(transform) + self.ellipse = Ellipse((0, 0), width, height, angle) + self._box.add_artist(self.ellipse) + + AnchoredOffsetbox.__init__(self, loc, pad=pad, borderpad=borderpad, + child=self._box, + prop=prop, + frameon=frameon, **kwargs) + + +class AnchoredSizeBar(AnchoredOffsetbox): + @docstring.dedent + def __init__(self, transform, size, label, loc, + pad=0.1, borderpad=0.1, sep=2, + frameon=True, size_vertical=0, color='black', + label_top=False, fontproperties=None, fill_bar=None, + **kwargs): + """ + Draw a horizontal scale bar with a center-aligned label underneath. + + Parameters + ---------- + transform : `matplotlib.transforms.Transform` + The transformation object for the coordinate system in use, i.e., + :attr:`matplotlib.axes.Axes.transData`. + + size : int or float + Horizontal length of the size bar, given in coordinates of + *transform*. + + label : str + Label to display. + + loc : int + Location of this size bar. Valid location codes are:: + + 'upper right' : 1, + 'upper left' : 2, + 'lower left' : 3, + 'lower right' : 4, + 'right' : 5, + 'center left' : 6, + 'center right' : 7, + 'lower center' : 8, + 'upper center' : 9, + 'center' : 10 + + pad : int or float, optional + Padding around the label and size bar, in fraction of the font + size. Defaults to 0.1. + + borderpad : int or float, optional + Border padding, in fraction of the font size. + Defaults to 0.1. + + sep : int or float, optional + Separation between the label and the size bar, in points. + Defaults to 2. + + frameon : bool, optional + If True, draw a box around the horizontal bar and label. + Defaults to True. + + size_vertical : int or float, optional + Vertical length of the size bar, given in coordinates of + *transform*. Defaults to 0. + + color : str, optional + Color for the size bar and label. + Defaults to black. + + label_top : bool, optional + If True, the label will be over the size bar. + Defaults to False. + + fontproperties : `matplotlib.font_manager.FontProperties`, optional + Font properties for the label text. + + fill_bar : bool, optional + If True and if size_vertical is nonzero, the size bar will + be filled in with the color specified by the size bar. + Defaults to True if `size_vertical` is greater than + zero and False otherwise. + + **kwargs : + Keyworded arguments to pass to + :class:`matplotlib.offsetbox.AnchoredOffsetbox`. + + Attributes + ---------- + size_bar : `matplotlib.offsetbox.AuxTransformBox` + Container for the size bar. + + txt_label : `matplotlib.offsetbox.TextArea` + Container for the label of the size bar. + + Notes + ----- + If *prop* is passed as a keyworded argument, but *fontproperties* is + not, then *prop* is be assumed to be the intended *fontproperties*. + Using both *prop* and *fontproperties* is not supported. + + Examples + -------- + >>> import matplotlib.pyplot as plt + >>> import numpy as np + >>> from mpl_toolkits.axes_grid1.anchored_artists import \ +AnchoredSizeBar + >>> fig, ax = plt.subplots() + >>> ax.imshow(np.random.random((10,10))) + >>> bar = AnchoredSizeBar(ax.transData, 3, '3 data units', 4) + >>> ax.add_artist(bar) + >>> fig.show() + + Using all the optional parameters + + >>> import matplotlib.font_manager as fm + >>> fontprops = fm.FontProperties(size=14, family='monospace') + >>> bar = AnchoredSizeBar(ax.transData, 3, '3 units', 4, pad=0.5, \ +sep=5, borderpad=0.5, frameon=False, \ +size_vertical=0.5, color='white', \ +fontproperties=fontprops) + """ + if fill_bar is None: + fill_bar = size_vertical > 0 + + self.size_bar = AuxTransformBox(transform) + self.size_bar.add_artist(Rectangle((0, 0), size, size_vertical, + fill=fill_bar, facecolor=color, + edgecolor=color)) + + if fontproperties is None and 'prop' in kwargs: + fontproperties = kwargs.pop('prop') + + if fontproperties is None: + textprops = {'color': color} + else: + textprops = {'color': color, 'fontproperties': fontproperties} + + self.txt_label = TextArea( + label, + minimumdescent=False, + textprops=textprops) + + if label_top: + _box_children = [self.txt_label, self.size_bar] + else: + _box_children = [self.size_bar, self.txt_label] + + self._box = VPacker(children=_box_children, + align="center", + pad=0, sep=sep) + + AnchoredOffsetbox.__init__(self, loc, pad=pad, borderpad=borderpad, + child=self._box, + prop=fontproperties, + frameon=frameon, **kwargs) diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/axes_divider.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/axes_divider.py new file mode 100644 index 0000000000..b238e73cc5 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/axes_divider.py @@ -0,0 +1,975 @@ +""" +The axes_divider module provides helper classes to adjust the positions of +multiple axes at drawing time. + + Divider: this is the class that is used to calculate the axes + position. It divides the given rectangular area into several sub + rectangles. You initialize the divider by setting the horizontal + and vertical lists of sizes that the division will be based on. You + then use the new_locator method, whose return value is a callable + object that can be used to set the axes_locator of the axes. + +""" +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six +from six.moves import map + +import matplotlib.transforms as mtransforms + +from matplotlib.axes import SubplotBase + +from . import axes_size as Size + + +class Divider(object): + """ + This class calculates the axes position. It + divides the given rectangular area into several + sub-rectangles. You initialize the divider by setting the + horizontal and vertical lists of sizes + (:mod:`mpl_toolkits.axes_grid.axes_size`) that the division will + be based on. You then use the new_locator method to create a + callable object that can be used as the axes_locator of the + axes. + """ + + def __init__(self, fig, pos, horizontal, vertical, + aspect=None, anchor="C"): + """ + Parameters + ---------- + fig : Figure + pos : tuple of 4 floats + position of the rectangle that will be divided + horizontal : list of :mod:`~mpl_toolkits.axes_grid.axes_size` + sizes for horizontal division + vertical : list of :mod:`~mpl_toolkits.axes_grid.axes_size` + sizes for vertical division + aspect : bool + if True, the overall rectangular area is reduced + so that the relative part of the horizontal and + vertical scales have the same scale. + anchor : {'C', 'SW', 'S', 'SE', 'E', 'NE', 'N', 'NW', 'W'} + placement of the reduced rectangle when *aspect* is True + """ + + self._fig = fig + self._pos = pos + self._horizontal = horizontal + self._vertical = vertical + self._anchor = anchor + self._aspect = aspect + self._xrefindex = 0 + self._yrefindex = 0 + self._locator = None + + def get_horizontal_sizes(self, renderer): + return [s.get_size(renderer) for s in self.get_horizontal()] + + def get_vertical_sizes(self, renderer): + return [s.get_size(renderer) for s in self.get_vertical()] + + def get_vsize_hsize(self): + + from .axes_size import AddList + + vsize = AddList(self.get_vertical()) + hsize = AddList(self.get_horizontal()) + + return vsize, hsize + + @staticmethod + def _calc_k(l, total_size): + + rs_sum, as_sum = 0., 0. + + for _rs, _as in l: + rs_sum += _rs + as_sum += _as + + if rs_sum != 0.: + k = (total_size - as_sum) / rs_sum + return k + else: + return 0. + + @staticmethod + def _calc_offsets(l, k): + + offsets = [0.] + + #for s in l: + for _rs, _as in l: + #_rs, _as = s.get_size(renderer) + offsets.append(offsets[-1] + _rs*k + _as) + + return offsets + + def set_position(self, pos): + """ + set the position of the rectangle. + + Parameters + ---------- + pos : tuple of 4 floats + position of the rectangle that will be divided + """ + self._pos = pos + + def get_position(self): + "return the position of the rectangle." + return self._pos + + def set_anchor(self, anchor): + """ + Parameters + ---------- + anchor : {'C', 'SW', 'S', 'SE', 'E', 'NE', 'N', 'NW', 'W'} + anchor position + + ===== ============ + value description + ===== ============ + 'C' Center + 'SW' bottom left + 'S' bottom + 'SE' bottom right + 'E' right + 'NE' top right + 'N' top + 'NW' top left + 'W' left + ===== ============ + + """ + if anchor in mtransforms.Bbox.coefs or len(anchor) == 2: + self._anchor = anchor + else: + raise ValueError('argument must be among %s' % + ', '.join(mtransforms.BBox.coefs)) + + def get_anchor(self): + "return the anchor" + return self._anchor + + def set_horizontal(self, h): + """ + Parameters + ---------- + h : list of :mod:`~mpl_toolkits.axes_grid.axes_size` + sizes for horizontal division + """ + self._horizontal = h + + def get_horizontal(self): + "return horizontal sizes" + return self._horizontal + + def set_vertical(self, v): + """ + Parameters + ---------- + v : list of :mod:`~mpl_toolkits.axes_grid.axes_size` + sizes for vertical division + """ + self._vertical = v + + def get_vertical(self): + "return vertical sizes" + return self._vertical + + def set_aspect(self, aspect=False): + """ + Parameters + ---------- + aspect : bool + """ + self._aspect = aspect + + def get_aspect(self): + "return aspect" + return self._aspect + + def set_locator(self, _locator): + self._locator = _locator + + def get_locator(self): + return self._locator + + def get_position_runtime(self, ax, renderer): + if self._locator is None: + return self.get_position() + else: + return self._locator(ax, renderer).bounds + + def locate(self, nx, ny, nx1=None, ny1=None, axes=None, renderer=None): + """ + Parameters + ---------- + nx, nx1 : int + Integers specifying the column-position of the + cell. When *nx1* is None, a single *nx*-th column is + specified. Otherwise location of columns spanning between *nx* + to *nx1* (but excluding *nx1*-th column) is specified. + ny, ny1 : int + Same as *nx* and *nx1*, but for row positions. + axes + renderer + """ + + figW, figH = self._fig.get_size_inches() + x, y, w, h = self.get_position_runtime(axes, renderer) + + hsizes = self.get_horizontal_sizes(renderer) + vsizes = self.get_vertical_sizes(renderer) + k_h = self._calc_k(hsizes, figW*w) + k_v = self._calc_k(vsizes, figH*h) + + if self.get_aspect(): + k = min(k_h, k_v) + ox = self._calc_offsets(hsizes, k) + oy = self._calc_offsets(vsizes, k) + + ww = (ox[-1] - ox[0])/figW + hh = (oy[-1] - oy[0])/figH + pb = mtransforms.Bbox.from_bounds(x, y, w, h) + pb1 = mtransforms.Bbox.from_bounds(x, y, ww, hh) + pb1_anchored = pb1.anchored(self.get_anchor(), pb) + x0, y0 = pb1_anchored.x0, pb1_anchored.y0 + + else: + ox = self._calc_offsets(hsizes, k_h) + oy = self._calc_offsets(vsizes, k_v) + x0, y0 = x, y + + if nx1 is None: + nx1 = nx+1 + if ny1 is None: + ny1 = ny+1 + + x1, w1 = x0 + ox[nx]/figW, (ox[nx1] - ox[nx])/figW + y1, h1 = y0 + oy[ny]/figH, (oy[ny1] - oy[ny])/figH + + return mtransforms.Bbox.from_bounds(x1, y1, w1, h1) + + def new_locator(self, nx, ny, nx1=None, ny1=None): + """ + Returns a new locator + (:class:`mpl_toolkits.axes_grid.axes_divider.AxesLocator`) for + specified cell. + + Parameters + ---------- + nx, nx1 : int + Integers specifying the column-position of the + cell. When *nx1* is None, a single *nx*-th column is + specified. Otherwise location of columns spanning between *nx* + to *nx1* (but excluding *nx1*-th column) is specified. + ny, ny1 : int + Same as *nx* and *nx1*, but for row positions. + """ + return AxesLocator(self, nx, ny, nx1, ny1) + + def append_size(self, position, size): + + if position == "left": + self._horizontal.insert(0, size) + self._xrefindex += 1 + elif position == "right": + self._horizontal.append(size) + elif position == "bottom": + self._vertical.insert(0, size) + self._yrefindex += 1 + elif position == "top": + self._vertical.append(size) + else: + raise ValueError("the position must be one of left," + + " right, bottom, or top") + + def add_auto_adjustable_area(self, + use_axes, pad=0.1, + adjust_dirs=None, + ): + if adjust_dirs is None: + adjust_dirs = ["left", "right", "bottom", "top"] + from .axes_size import Padded, SizeFromFunc, GetExtentHelper + for d in adjust_dirs: + helper = GetExtentHelper(use_axes, d) + size = SizeFromFunc(helper) + padded_size = Padded(size, pad) # pad in inch + self.append_size(d, padded_size) + + +class AxesLocator(object): + """ + A simple callable object, initialized with AxesDivider class, + returns the position and size of the given cell. + """ + def __init__(self, axes_divider, nx, ny, nx1=None, ny1=None): + """ + Parameters + ---------- + axes_divider : AxesDivider + nx, nx1 : int + Integers specifying the column-position of the + cell. When *nx1* is None, a single *nx*-th column is + specified. Otherwise location of columns spanning between *nx* + to *nx1* (but excluding *nx1*-th column) is specified. + ny, ny1 : int + Same as *nx* and *nx1*, but for row positions. + """ + self._axes_divider = axes_divider + + _xrefindex = axes_divider._xrefindex + _yrefindex = axes_divider._yrefindex + + self._nx, self._ny = nx - _xrefindex, ny - _yrefindex + + if nx1 is None: + nx1 = nx+1 + if ny1 is None: + ny1 = ny+1 + + self._nx1 = nx1 - _xrefindex + self._ny1 = ny1 - _yrefindex + + def __call__(self, axes, renderer): + + _xrefindex = self._axes_divider._xrefindex + _yrefindex = self._axes_divider._yrefindex + + return self._axes_divider.locate(self._nx + _xrefindex, + self._ny + _yrefindex, + self._nx1 + _xrefindex, + self._ny1 + _yrefindex, + axes, + renderer) + + def get_subplotspec(self): + if hasattr(self._axes_divider, "get_subplotspec"): + return self._axes_divider.get_subplotspec() + else: + return None + + +from matplotlib.gridspec import SubplotSpec, GridSpec + + +class SubplotDivider(Divider): + """ + The Divider class whose rectangle area is specified as a subplot geometry. + """ + + def __init__(self, fig, *args, **kwargs): + """ + Parameters + ---------- + fig : :class:`matplotlib.figure.Figure` + args : tuple (*numRows*, *numCols*, *plotNum*) + The array of subplots in the figure has dimensions *numRows*, + *numCols*, and *plotNum* is the number of the subplot + being created. *plotNum* starts at 1 in the upper left + corner and increases to the right. + + If *numRows* <= *numCols* <= *plotNum* < 10, *args* can be the + decimal integer *numRows* * 100 + *numCols* * 10 + *plotNum*. + """ + + self.figure = fig + + if len(args) == 1: + if isinstance(args[0], SubplotSpec): + self._subplotspec = args[0] + else: + try: + s = str(int(args[0])) + rows, cols, num = map(int, s) + except ValueError: + raise ValueError( + 'Single argument to subplot must be a 3-digit integer') + self._subplotspec = GridSpec(rows, cols)[num-1] + # num - 1 for converting from MATLAB to python indexing + elif len(args) == 3: + rows, cols, num = args + rows = int(rows) + cols = int(cols) + if isinstance(num, tuple) and len(num) == 2: + num = [int(n) for n in num] + self._subplotspec = GridSpec(rows, cols)[num[0]-1:num[1]] + else: + self._subplotspec = GridSpec(rows, cols)[int(num)-1] + # num - 1 for converting from MATLAB to python indexing + else: + raise ValueError('Illegal argument(s) to subplot: %s' % (args,)) + + # total = rows*cols + # num -= 1 # convert from matlab to python indexing + # # i.e., num in range(0,total) + # if num >= total: + # raise ValueError( 'Subplot number exceeds total subplots') + # self._rows = rows + # self._cols = cols + # self._num = num + + # self.update_params() + + # sets self.fixbox + self.update_params() + + pos = self.figbox.bounds + + horizontal = kwargs.pop("horizontal", []) + vertical = kwargs.pop("vertical", []) + aspect = kwargs.pop("aspect", None) + anchor = kwargs.pop("anchor", "C") + + if kwargs: + raise Exception("") + + Divider.__init__(self, fig, pos, horizontal, vertical, + aspect=aspect, anchor=anchor) + + def get_position(self): + "return the bounds of the subplot box" + + self.update_params() # update self.figbox + return self.figbox.bounds + + # def update_params(self): + # 'update the subplot position from fig.subplotpars' + + # rows = self._rows + # cols = self._cols + # num = self._num + + # pars = self.figure.subplotpars + # left = pars.left + # right = pars.right + # bottom = pars.bottom + # top = pars.top + # wspace = pars.wspace + # hspace = pars.hspace + # totWidth = right-left + # totHeight = top-bottom + + # figH = totHeight/(rows + hspace*(rows-1)) + # sepH = hspace*figH + + # figW = totWidth/(cols + wspace*(cols-1)) + # sepW = wspace*figW + + # rowNum, colNum = divmod(num, cols) + + # figBottom = top - (rowNum+1)*figH - rowNum*sepH + # figLeft = left + colNum*(figW + sepW) + + # self.figbox = mtransforms.Bbox.from_bounds(figLeft, figBottom, + # figW, figH) + + def update_params(self): + 'update the subplot position from fig.subplotpars' + + self.figbox = self.get_subplotspec().get_position(self.figure) + + def get_geometry(self): + 'get the subplot geometry, e.g., 2,2,3' + rows, cols, num1, num2 = self.get_subplotspec().get_geometry() + return rows, cols, num1+1 # for compatibility + + # COVERAGE NOTE: Never used internally or from examples + def change_geometry(self, numrows, numcols, num): + 'change subplot geometry, e.g., from 1,1,1 to 2,2,3' + self._subplotspec = GridSpec(numrows, numcols)[num-1] + self.update_params() + self.set_position(self.figbox) + + def get_subplotspec(self): + 'get the SubplotSpec instance' + return self._subplotspec + + def set_subplotspec(self, subplotspec): + 'set the SubplotSpec instance' + self._subplotspec = subplotspec + + +class AxesDivider(Divider): + """ + Divider based on the pre-existing axes. + """ + + def __init__(self, axes, xref=None, yref=None): + """ + Parameters + ---------- + axes : :class:`~matplotlib.axes.Axes` + xref + yref + """ + self._axes = axes + if xref is None: + self._xref = Size.AxesX(axes) + else: + self._xref = xref + if yref is None: + self._yref = Size.AxesY(axes) + else: + self._yref = yref + + Divider.__init__(self, fig=axes.get_figure(), pos=None, + horizontal=[self._xref], vertical=[self._yref], + aspect=None, anchor="C") + + def _get_new_axes(self, **kwargs): + axes = self._axes + + axes_class = kwargs.pop("axes_class", None) + + if axes_class is None: + if isinstance(axes, SubplotBase): + axes_class = axes._axes_class + else: + axes_class = type(axes) + + ax = axes_class(axes.get_figure(), + axes.get_position(original=True), **kwargs) + + return ax + + def new_horizontal(self, size, pad=None, pack_start=False, **kwargs): + """ + Add a new axes on the right (or left) side of the main axes. + + Parameters + ---------- + size : :mod:`~mpl_toolkits.axes_grid.axes_size` or float or string + A width of the axes. If float or string is given, *from_any* + function is used to create the size, with *ref_size* set to AxesX + instance of the current axes. + pad : :mod:`~mpl_toolkits.axes_grid.axes_size` or float or string + Pad between the axes. It takes same argument as *size*. + pack_start : bool + If False, the new axes is appended at the end + of the list, i.e., it became the right-most axes. If True, it is + inserted at the start of the list, and becomes the left-most axes. + kwargs + All extra keywords arguments are passed to the created axes. + If *axes_class* is given, the new axes will be created as an + instance of the given class. Otherwise, the same class of the + main axes will be used. + """ + + if pad: + if not isinstance(pad, Size._Base): + pad = Size.from_any(pad, + fraction_ref=self._xref) + if pack_start: + self._horizontal.insert(0, pad) + self._xrefindex += 1 + else: + self._horizontal.append(pad) + + if not isinstance(size, Size._Base): + size = Size.from_any(size, + fraction_ref=self._xref) + + if pack_start: + self._horizontal.insert(0, size) + self._xrefindex += 1 + locator = self.new_locator(nx=0, ny=self._yrefindex) + else: + self._horizontal.append(size) + locator = self.new_locator(nx=len(self._horizontal)-1, ny=self._yrefindex) + + ax = self._get_new_axes(**kwargs) + ax.set_axes_locator(locator) + + return ax + + def new_vertical(self, size, pad=None, pack_start=False, **kwargs): + """ + Add a new axes on the top (or bottom) side of the main axes. + + Parameters + ---------- + size : :mod:`~mpl_toolkits.axes_grid.axes_size` or float or string + A height of the axes. If float or string is given, *from_any* + function is used to create the size, with *ref_size* set to AxesX + instance of the current axes. + pad : :mod:`~mpl_toolkits.axes_grid.axes_size` or float or string + Pad between the axes. It takes same argument as *size*. + pack_start : bool + If False, the new axes is appended at the end + of the list, i.e., it became the right-most axes. If True, it is + inserted at the start of the list, and becomes the left-most axes. + kwargs + All extra keywords arguments are passed to the created axes. + If *axes_class* is given, the new axes will be created as an + instance of the given class. Otherwise, the same class of the + main axes will be used. + """ + + if pad: + if not isinstance(pad, Size._Base): + pad = Size.from_any(pad, + fraction_ref=self._yref) + if pack_start: + self._vertical.insert(0, pad) + self._yrefindex += 1 + else: + self._vertical.append(pad) + + if not isinstance(size, Size._Base): + size = Size.from_any(size, + fraction_ref=self._yref) + + if pack_start: + self._vertical.insert(0, size) + self._yrefindex += 1 + locator = self.new_locator(nx=self._xrefindex, ny=0) + else: + self._vertical.append(size) + locator = self.new_locator(nx=self._xrefindex, ny=len(self._vertical)-1) + + ax = self._get_new_axes(**kwargs) + ax.set_axes_locator(locator) + + return ax + + def append_axes(self, position, size, pad=None, add_to_figure=True, + **kwargs): + """ + create an axes at the given *position* with the same height + (or width) of the main axes. + + *position* + ["left"|"right"|"bottom"|"top"] + + *size* and *pad* should be axes_grid.axes_size compatible. + """ + + if position == "left": + ax = self.new_horizontal(size, pad, pack_start=True, **kwargs) + elif position == "right": + ax = self.new_horizontal(size, pad, pack_start=False, **kwargs) + elif position == "bottom": + ax = self.new_vertical(size, pad, pack_start=True, **kwargs) + elif position == "top": + ax = self.new_vertical(size, pad, pack_start=False, **kwargs) + else: + raise ValueError("the position must be one of left," + + " right, bottom, or top") + + if add_to_figure: + self._fig.add_axes(ax) + return ax + + def get_aspect(self): + if self._aspect is None: + aspect = self._axes.get_aspect() + if aspect == "auto": + return False + else: + return True + else: + return self._aspect + + def get_position(self): + if self._pos is None: + bbox = self._axes.get_position(original=True) + return bbox.bounds + else: + return self._pos + + def get_anchor(self): + if self._anchor is None: + return self._axes.get_anchor() + else: + return self._anchor + + def get_subplotspec(self): + if hasattr(self._axes, "get_subplotspec"): + return self._axes.get_subplotspec() + else: + return None + + +class HBoxDivider(SubplotDivider): + + def __init__(self, fig, *args, **kwargs): + SubplotDivider.__init__(self, fig, *args, **kwargs) + + @staticmethod + def _determine_karray(equivalent_sizes, appended_sizes, + max_equivalent_size, + total_appended_size): + + n = len(equivalent_sizes) + import numpy as np + A = np.mat(np.zeros((n+1, n+1), dtype="d")) + B = np.zeros((n+1), dtype="d") + # AxK = B + + # populated A + for i, (r, a) in enumerate(equivalent_sizes): + A[i, i] = r + A[i, -1] = -1 + B[i] = -a + A[-1, :-1] = [r for r, a in appended_sizes] + B[-1] = total_appended_size - sum([a for rs, a in appended_sizes]) + + karray_H = (A.I*np.mat(B).T).A1 + karray = karray_H[:-1] + H = karray_H[-1] + + if H > max_equivalent_size: + karray = ((max_equivalent_size - + np.array([a for r, a in equivalent_sizes])) + / np.array([r for r, a in equivalent_sizes])) + return karray + + @staticmethod + def _calc_offsets(appended_sizes, karray): + offsets = [0.] + + #for s in l: + for (r, a), k in zip(appended_sizes, karray): + offsets.append(offsets[-1] + r*k + a) + + return offsets + + def new_locator(self, nx, nx1=None): + """ + returns a new locator + (:class:`mpl_toolkits.axes_grid.axes_divider.AxesLocator`) for + specified cell. + + Parameters + ---------- + nx, nx1 : int + Integers specifying the column-position of the + cell. When *nx1* is None, a single *nx*-th column is + specified. Otherwise location of columns spanning between *nx* + to *nx1* (but excluding *nx1*-th column) is specified. + ny, ny1 : int + Same as *nx* and *nx1*, but for row positions. + """ + return AxesLocator(self, nx, 0, nx1, None) + + def _locate(self, x, y, w, h, + y_equivalent_sizes, x_appended_sizes, + figW, figH): + """ + Parameters + ---------- + x + y + w + h + y_equivalent_sizes + x_appended_sizes + figW + figH + """ + + equivalent_sizes = y_equivalent_sizes + appended_sizes = x_appended_sizes + + max_equivalent_size = figH*h + total_appended_size = figW*w + karray = self._determine_karray(equivalent_sizes, appended_sizes, + max_equivalent_size, + total_appended_size) + + ox = self._calc_offsets(appended_sizes, karray) + + ww = (ox[-1] - ox[0])/figW + ref_h = equivalent_sizes[0] + hh = (karray[0]*ref_h[0] + ref_h[1])/figH + pb = mtransforms.Bbox.from_bounds(x, y, w, h) + pb1 = mtransforms.Bbox.from_bounds(x, y, ww, hh) + pb1_anchored = pb1.anchored(self.get_anchor(), pb) + x0, y0 = pb1_anchored.x0, pb1_anchored.y0 + + return x0, y0, ox, hh + + def locate(self, nx, ny, nx1=None, ny1=None, axes=None, renderer=None): + """ + Parameters + ---------- + axes_divider : AxesDivider + nx, nx1 : int + Integers specifying the column-position of the + cell. When *nx1* is None, a single *nx*-th column is + specified. Otherwise location of columns spanning between *nx* + to *nx1* (but excluding *nx1*-th column) is specified. + ny, ny1 : int + Same as *nx* and *nx1*, but for row positions. + axes + renderer + """ + + figW, figH = self._fig.get_size_inches() + x, y, w, h = self.get_position_runtime(axes, renderer) + + y_equivalent_sizes = self.get_vertical_sizes(renderer) + x_appended_sizes = self.get_horizontal_sizes(renderer) + x0, y0, ox, hh = self._locate(x, y, w, h, + y_equivalent_sizes, x_appended_sizes, + figW, figH) + if nx1 is None: + nx1 = nx+1 + + x1, w1 = x0 + ox[nx]/figW, (ox[nx1] - ox[nx])/figW + y1, h1 = y0, hh + + return mtransforms.Bbox.from_bounds(x1, y1, w1, h1) + + +class VBoxDivider(HBoxDivider): + """ + The Divider class whose rectangle area is specified as a subplot geometry. + """ + + def new_locator(self, ny, ny1=None): + """ + returns a new locator + (:class:`mpl_toolkits.axes_grid.axes_divider.AxesLocator`) for + specified cell. + + Parameters + ---------- + ny, ny1 : int + Integers specifying the row-position of the + cell. When *ny1* is None, a single *ny*-th row is + specified. Otherwise location of rows spanning between *ny* + to *ny1* (but excluding *ny1*-th row) is specified. + """ + return AxesLocator(self, 0, ny, None, ny1) + + def locate(self, nx, ny, nx1=None, ny1=None, axes=None, renderer=None): + """ + Parameters + ---------- + axes_divider : AxesDivider + nx, nx1 : int + Integers specifying the column-position of the + cell. When *nx1* is None, a single *nx*-th column is + specified. Otherwise location of columns spanning between *nx* + to *nx1* (but excluding *nx1*-th column) is specified. + ny, ny1 : int + Same as *nx* and *nx1*, but for row positions. + axes + renderer + """ + + figW, figH = self._fig.get_size_inches() + x, y, w, h = self.get_position_runtime(axes, renderer) + + x_equivalent_sizes = self.get_horizontal_sizes(renderer) + y_appended_sizes = self.get_vertical_sizes(renderer) + + y0, x0, oy, ww = self._locate(y, x, h, w, + x_equivalent_sizes, y_appended_sizes, + figH, figW) + if ny1 is None: + ny1 = ny+1 + + x1, w1 = x0, ww + y1, h1 = y0 + oy[ny]/figH, (oy[ny1] - oy[ny])/figH + + return mtransforms.Bbox.from_bounds(x1, y1, w1, h1) + + +class LocatableAxesBase(object): + def __init__(self, *kl, **kw): + + self._axes_class.__init__(self, *kl, **kw) + + self._locator = None + self._locator_renderer = None + + def set_axes_locator(self, locator): + self._locator = locator + + def get_axes_locator(self): + return self._locator + + def apply_aspect(self, position=None): + + if self.get_axes_locator() is None: + self._axes_class.apply_aspect(self, position) + else: + pos = self.get_axes_locator()(self, self._locator_renderer) + self._axes_class.apply_aspect(self, position=pos) + + def draw(self, renderer=None, inframe=False): + + self._locator_renderer = renderer + + self._axes_class.draw(self, renderer, inframe) + + def _make_twin_axes(self, *kl, **kwargs): + """ + Need to overload so that twinx/twiny will work with + these axes. + """ + if 'sharex' in kwargs and 'sharey' in kwargs: + raise ValueError("Twinned Axes may share only one axis.") + ax2 = type(self)(self.figure, self.get_position(True), *kl, **kwargs) + ax2.set_axes_locator(self.get_axes_locator()) + self.figure.add_axes(ax2) + self.set_adjustable('datalim') + ax2.set_adjustable('datalim') + self._twinned_axes.join(self, ax2) + return ax2 + +_locatableaxes_classes = {} + + +def locatable_axes_factory(axes_class): + + new_class = _locatableaxes_classes.get(axes_class) + if new_class is None: + new_class = type(str("Locatable%s" % (axes_class.__name__)), + (LocatableAxesBase, axes_class), + {'_axes_class': axes_class}) + + _locatableaxes_classes[axes_class] = new_class + + return new_class + +#if hasattr(maxes.Axes, "get_axes_locator"): +# LocatableAxes = maxes.Axes +#else: + + +def make_axes_locatable(axes): + if not hasattr(axes, "set_axes_locator"): + new_class = locatable_axes_factory(type(axes)) + axes.__class__ = new_class + + divider = AxesDivider(axes) + locator = divider.new_locator(nx=0, ny=0) + axes.set_axes_locator(locator) + + return divider + + +def make_axes_area_auto_adjustable(ax, + use_axes=None, pad=0.1, + adjust_dirs=None): + if adjust_dirs is None: + adjust_dirs = ["left", "right", "bottom", "top"] + divider = make_axes_locatable(ax) + + if use_axes is None: + use_axes = ax + + divider.add_auto_adjustable_area(use_axes=use_axes, pad=pad, + adjust_dirs=adjust_dirs) + +#from matplotlib.axes import Axes +from .mpl_axes import Axes +LocatableAxes = locatable_axes_factory(Axes) diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/axes_grid.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/axes_grid.py new file mode 100644 index 0000000000..dde0e8dd7c --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/axes_grid.py @@ -0,0 +1,771 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six + +import matplotlib.axes as maxes +import matplotlib.cbook as cbook +import matplotlib.ticker as ticker +from matplotlib.gridspec import SubplotSpec + +from .axes_divider import Size, SubplotDivider, LocatableAxes, Divider +from .colorbar import Colorbar + + +def _extend_axes_pad(value): + # Check whether a list/tuple/array or scalar has been passed + ret = value + if not hasattr(ret, "__getitem__"): + ret = (value, value) + return ret + + +def _tick_only(ax, bottom_on, left_on): + bottom_off = not bottom_on + left_off = not left_on + # [l.set_visible(bottom_off) for l in ax.get_xticklabels()] + # [l.set_visible(left_off) for l in ax.get_yticklabels()] + # ax.xaxis.label.set_visible(bottom_off) + # ax.yaxis.label.set_visible(left_off) + ax.axis["bottom"].toggle(ticklabels=bottom_off, label=bottom_off) + ax.axis["left"].toggle(ticklabels=left_off, label=left_off) + + +class CbarAxesBase(object): + + def colorbar(self, mappable, **kwargs): + locator = kwargs.pop("locator", None) + + if locator is None: + if "ticks" not in kwargs: + kwargs["ticks"] = ticker.MaxNLocator(5) + if locator is not None: + if "ticks" in kwargs: + raise ValueError("Either *locator* or *ticks* need" + + " to be given, not both") + else: + kwargs["ticks"] = locator + + self._hold = True + if self.orientation in ["top", "bottom"]: + orientation = "horizontal" + else: + orientation = "vertical" + + cb = Colorbar(self, mappable, orientation=orientation, **kwargs) + self._config_axes() + + def on_changed(m): + cb.set_cmap(m.get_cmap()) + cb.set_clim(m.get_clim()) + cb.update_bruteforce(m) + + self.cbid = mappable.callbacksSM.connect('changed', on_changed) + mappable.colorbar = cb + + self.locator = cb.cbar_axis.get_major_locator() + + return cb + + def _config_axes(self): + ''' + Make an axes patch and outline. + ''' + ax = self + ax.set_navigate(False) + + ax.axis[:].toggle(all=False) + b = self._default_label_on + ax.axis[self.orientation].toggle(all=b) + + # for axis in ax.axis.values(): + # axis.major_ticks.set_visible(False) + # axis.minor_ticks.set_visible(False) + # axis.major_ticklabels.set_visible(False) + # axis.minor_ticklabels.set_visible(False) + # axis.label.set_visible(False) + + # axis = ax.axis[self.orientation] + # axis.major_ticks.set_visible(True) + # axis.minor_ticks.set_visible(True) + + #axis.major_ticklabels.set_size( + # int(axis.major_ticklabels.get_size()*.9)) + #axis.major_tick_pad = 3 + + # axis.major_ticklabels.set_visible(b) + # axis.minor_ticklabels.set_visible(b) + # axis.label.set_visible(b) + + def toggle_label(self, b): + self._default_label_on = b + axis = self.axis[self.orientation] + axis.toggle(ticklabels=b, label=b) + #axis.major_ticklabels.set_visible(b) + #axis.minor_ticklabels.set_visible(b) + #axis.label.set_visible(b) + + +class CbarAxes(CbarAxesBase, LocatableAxes): + def __init__(self, *kl, **kwargs): + orientation = kwargs.pop("orientation", None) + if orientation is None: + raise ValueError("orientation must be specified") + self.orientation = orientation + self._default_label_on = True + self.locator = None + + super(LocatableAxes, self).__init__(*kl, **kwargs) + + def cla(self): + super(LocatableAxes, self).cla() + self._config_axes() + + +class Grid(object): + """ + A class that creates a grid of Axes. In matplotlib, the axes + location (and size) is specified in the normalized figure + coordinates. This may not be ideal for images that needs to be + displayed with a given aspect ratio. For example, displaying + images of a same size with some fixed padding between them cannot + be easily done in matplotlib. AxesGrid is used in such case. + """ + + _defaultLocatableAxesClass = LocatableAxes + + def __init__(self, fig, + rect, + nrows_ncols, + ngrids=None, + direction="row", + axes_pad=0.02, + add_all=True, + share_all=False, + share_x=True, + share_y=True, + #aspect=True, + label_mode="L", + axes_class=None, + ): + """ + Build an :class:`Grid` instance with a grid nrows*ncols + :class:`~matplotlib.axes.Axes` in + :class:`~matplotlib.figure.Figure` *fig* with + *rect=[left, bottom, width, height]* (in + :class:`~matplotlib.figure.Figure` coordinates) or + the subplot position code (e.g., "121"). + + Optional keyword arguments: + + ================ ======== ========================================= + Keyword Default Description + ================ ======== ========================================= + direction "row" [ "row" | "column" ] + axes_pad 0.02 float| pad between axes given in inches + or tuple-like of floats, + (horizontal padding, vertical padding) + add_all True bool + share_all False bool + share_x True bool + share_y True bool + label_mode "L" [ "L" | "1" | "all" ] + axes_class None a type object which must be a subclass + of :class:`~matplotlib.axes.Axes` + ================ ======== ========================================= + """ + self._nrows, self._ncols = nrows_ncols + + if ngrids is None: + ngrids = self._nrows * self._ncols + else: + if (ngrids > self._nrows * self._ncols) or (ngrids <= 0): + raise Exception("") + + self.ngrids = ngrids + + self._init_axes_pad(axes_pad) + + if direction not in ["column", "row"]: + raise Exception("") + + self._direction = direction + + if axes_class is None: + axes_class = self._defaultLocatableAxesClass + axes_class_args = {} + else: + if (type(axes_class)) == type and \ + issubclass(axes_class, + self._defaultLocatableAxesClass.Axes): + axes_class_args = {} + else: + axes_class, axes_class_args = axes_class + + self.axes_all = [] + self.axes_column = [[] for _ in range(self._ncols)] + self.axes_row = [[] for _ in range(self._nrows)] + + h = [] + v = [] + if isinstance(rect, six.string_types) or cbook.is_numlike(rect): + self._divider = SubplotDivider(fig, rect, horizontal=h, vertical=v, + aspect=False) + elif isinstance(rect, SubplotSpec): + self._divider = SubplotDivider(fig, rect, horizontal=h, vertical=v, + aspect=False) + elif len(rect) == 3: + kw = dict(horizontal=h, vertical=v, aspect=False) + self._divider = SubplotDivider(fig, *rect, **kw) + elif len(rect) == 4: + self._divider = Divider(fig, rect, horizontal=h, vertical=v, + aspect=False) + else: + raise Exception("") + + rect = self._divider.get_position() + + # reference axes + self._column_refax = [None for _ in range(self._ncols)] + self._row_refax = [None for _ in range(self._nrows)] + self._refax = None + + for i in range(self.ngrids): + + col, row = self._get_col_row(i) + + if share_all: + sharex = self._refax + sharey = self._refax + else: + if share_x: + sharex = self._column_refax[col] + else: + sharex = None + + if share_y: + sharey = self._row_refax[row] + else: + sharey = None + + ax = axes_class(fig, rect, sharex=sharex, sharey=sharey, + **axes_class_args) + + if share_all: + if self._refax is None: + self._refax = ax + else: + if sharex is None: + self._column_refax[col] = ax + if sharey is None: + self._row_refax[row] = ax + + self.axes_all.append(ax) + self.axes_column[col].append(ax) + self.axes_row[row].append(ax) + + self.axes_llc = self.axes_column[0][-1] + + self._update_locators() + + if add_all: + for ax in self.axes_all: + fig.add_axes(ax) + + self.set_label_mode(label_mode) + + def _init_axes_pad(self, axes_pad): + axes_pad = _extend_axes_pad(axes_pad) + self._axes_pad = axes_pad + + self._horiz_pad_size = Size.Fixed(axes_pad[0]) + self._vert_pad_size = Size.Fixed(axes_pad[1]) + + def _update_locators(self): + + h = [] + + h_ax_pos = [] + + for _ in self._column_refax: + #if h: h.append(Size.Fixed(self._axes_pad)) + if h: + h.append(self._horiz_pad_size) + + h_ax_pos.append(len(h)) + + sz = Size.Scaled(1) + h.append(sz) + + v = [] + + v_ax_pos = [] + for _ in self._row_refax[::-1]: + #if v: v.append(Size.Fixed(self._axes_pad)) + if v: + v.append(self._vert_pad_size) + + v_ax_pos.append(len(v)) + sz = Size.Scaled(1) + v.append(sz) + + for i in range(self.ngrids): + col, row = self._get_col_row(i) + locator = self._divider.new_locator(nx=h_ax_pos[col], + ny=v_ax_pos[self._nrows - 1 - row]) + self.axes_all[i].set_axes_locator(locator) + + self._divider.set_horizontal(h) + self._divider.set_vertical(v) + + def _get_col_row(self, n): + if self._direction == "column": + col, row = divmod(n, self._nrows) + else: + row, col = divmod(n, self._ncols) + + return col, row + + # Good to propagate __len__ if we have __getitem__ + def __len__(self): + return len(self.axes_all) + + def __getitem__(self, i): + return self.axes_all[i] + + def get_geometry(self): + """ + get geometry of the grid. Returns a tuple of two integer, + representing number of rows and number of columns. + """ + return self._nrows, self._ncols + + def set_axes_pad(self, axes_pad): + "set axes_pad" + self._axes_pad = axes_pad + + # These two lines actually differ from ones in _init_axes_pad + self._horiz_pad_size.fixed_size = axes_pad[0] + self._vert_pad_size.fixed_size = axes_pad[1] + + def get_axes_pad(self): + """ + get axes_pad + + Returns + ------- + tuple + Padding in inches, (horizontal pad, vertical pad) + """ + return self._axes_pad + + def set_aspect(self, aspect): + "set aspect" + self._divider.set_aspect(aspect) + + def get_aspect(self): + "get aspect" + return self._divider.get_aspect() + + def set_label_mode(self, mode): + "set label_mode" + if mode == "all": + for ax in self.axes_all: + _tick_only(ax, False, False) + elif mode == "L": + # left-most axes + for ax in self.axes_column[0][:-1]: + _tick_only(ax, bottom_on=True, left_on=False) + # lower-left axes + ax = self.axes_column[0][-1] + _tick_only(ax, bottom_on=False, left_on=False) + + for col in self.axes_column[1:]: + # axes with no labels + for ax in col[:-1]: + _tick_only(ax, bottom_on=True, left_on=True) + + # bottom + ax = col[-1] + _tick_only(ax, bottom_on=False, left_on=True) + + elif mode == "1": + for ax in self.axes_all: + _tick_only(ax, bottom_on=True, left_on=True) + + ax = self.axes_llc + _tick_only(ax, bottom_on=False, left_on=False) + + def get_divider(self): + return self._divider + + def set_axes_locator(self, locator): + self._divider.set_locator(locator) + + def get_axes_locator(self): + return self._divider.get_locator() + + def get_vsize_hsize(self): + + return self._divider.get_vsize_hsize() +# from axes_size import AddList + +# vsize = AddList(self._divider.get_vertical()) +# hsize = AddList(self._divider.get_horizontal()) + +# return vsize, hsize + + +class ImageGrid(Grid): + """ + A class that creates a grid of Axes. In matplotlib, the axes + location (and size) is specified in the normalized figure + coordinates. This may not be ideal for images that needs to be + displayed with a given aspect ratio. For example, displaying + images of a same size with some fixed padding between them cannot + be easily done in matplotlib. ImageGrid is used in such case. + """ + + _defaultCbarAxesClass = CbarAxes + + def __init__(self, fig, + rect, + nrows_ncols, + ngrids=None, + direction="row", + axes_pad=0.02, + add_all=True, + share_all=False, + aspect=True, + label_mode="L", + cbar_mode=None, + cbar_location="right", + cbar_pad=None, + cbar_size="5%", + cbar_set_cax=True, + axes_class=None, + ): + """ + Build an :class:`ImageGrid` instance with a grid nrows*ncols + :class:`~matplotlib.axes.Axes` in + :class:`~matplotlib.figure.Figure` *fig* with + *rect=[left, bottom, width, height]* (in + :class:`~matplotlib.figure.Figure` coordinates) or + the subplot position code (e.g., "121"). + + Optional keyword arguments: + + ================ ======== ========================================= + Keyword Default Description + ================ ======== ========================================= + direction "row" [ "row" | "column" ] + axes_pad 0.02 float| pad between axes given in inches + or tuple-like of floats, + (horizontal padding, vertical padding) + add_all True bool + share_all False bool + aspect True bool + label_mode "L" [ "L" | "1" | "all" ] + cbar_mode None [ "each" | "single" | "edge" ] + cbar_location "right" [ "left" | "right" | "bottom" | "top" ] + cbar_pad None + cbar_size "5%" + cbar_set_cax True bool + axes_class None a type object which must be a subclass + of axes_grid's subclass of + :class:`~matplotlib.axes.Axes` + ================ ======== ========================================= + + *cbar_set_cax* : if True, each axes in the grid has a cax + attribute that is bind to associated cbar_axes. + """ + self._nrows, self._ncols = nrows_ncols + + if ngrids is None: + ngrids = self._nrows * self._ncols + else: + if not 0 <= ngrids < self._nrows * self._ncols: + raise Exception + + self.ngrids = ngrids + + axes_pad = _extend_axes_pad(axes_pad) + self._axes_pad = axes_pad + + self._colorbar_mode = cbar_mode + self._colorbar_location = cbar_location + if cbar_pad is None: + # horizontal or vertical arrangement? + if cbar_location in ("left", "right"): + self._colorbar_pad = axes_pad[0] + else: + self._colorbar_pad = axes_pad[1] + else: + self._colorbar_pad = cbar_pad + + self._colorbar_size = cbar_size + + self._init_axes_pad(axes_pad) + + if direction not in ["column", "row"]: + raise Exception("") + + self._direction = direction + + if axes_class is None: + axes_class = self._defaultLocatableAxesClass + axes_class_args = {} + else: + if isinstance(axes_class, maxes.Axes): + axes_class_args = {} + else: + axes_class, axes_class_args = axes_class + + self.axes_all = [] + self.axes_column = [[] for _ in range(self._ncols)] + self.axes_row = [[] for _ in range(self._nrows)] + + self.cbar_axes = [] + + h = [] + v = [] + if isinstance(rect, six.string_types) or cbook.is_numlike(rect): + self._divider = SubplotDivider(fig, rect, horizontal=h, vertical=v, + aspect=aspect) + elif isinstance(rect, SubplotSpec): + self._divider = SubplotDivider(fig, rect, horizontal=h, vertical=v, + aspect=aspect) + elif len(rect) == 3: + kw = dict(horizontal=h, vertical=v, aspect=aspect) + self._divider = SubplotDivider(fig, *rect, **kw) + elif len(rect) == 4: + self._divider = Divider(fig, rect, horizontal=h, vertical=v, + aspect=aspect) + else: + raise Exception("") + + rect = self._divider.get_position() + + # reference axes + self._column_refax = [None for _ in range(self._ncols)] + self._row_refax = [None for _ in range(self._nrows)] + self._refax = None + + for i in range(self.ngrids): + + col, row = self._get_col_row(i) + + if share_all: + if self.axes_all: + sharex = self.axes_all[0] + sharey = self.axes_all[0] + else: + sharex = None + sharey = None + else: + sharex = self._column_refax[col] + sharey = self._row_refax[row] + + ax = axes_class(fig, rect, sharex=sharex, sharey=sharey, + **axes_class_args) + + self.axes_all.append(ax) + self.axes_column[col].append(ax) + self.axes_row[row].append(ax) + + if share_all: + if self._refax is None: + self._refax = ax + if sharex is None: + self._column_refax[col] = ax + if sharey is None: + self._row_refax[row] = ax + + cax = self._defaultCbarAxesClass(fig, rect, + orientation=self._colorbar_location) + self.cbar_axes.append(cax) + + self.axes_llc = self.axes_column[0][-1] + + self._update_locators() + + if add_all: + for ax in self.axes_all+self.cbar_axes: + fig.add_axes(ax) + + if cbar_set_cax: + if self._colorbar_mode == "single": + for ax in self.axes_all: + ax.cax = self.cbar_axes[0] + elif self._colorbar_mode == "edge": + for index, ax in enumerate(self.axes_all): + col, row = self._get_col_row(index) + if self._colorbar_location in ("left", "right"): + ax.cax = self.cbar_axes[row] + else: + ax.cax = self.cbar_axes[col] + else: + for ax, cax in zip(self.axes_all, self.cbar_axes): + ax.cax = cax + + self.set_label_mode(label_mode) + + def _update_locators(self): + + h = [] + v = [] + + h_ax_pos = [] + h_cb_pos = [] + if (self._colorbar_mode == "single" and + self._colorbar_location in ('left', 'bottom')): + if self._colorbar_location == "left": + #sz = Size.Fraction(Size.AxesX(self.axes_llc), self._nrows) + sz = Size.Fraction(self._nrows, Size.AxesX(self.axes_llc)) + h.append(Size.from_any(self._colorbar_size, sz)) + h.append(Size.from_any(self._colorbar_pad, sz)) + locator = self._divider.new_locator(nx=0, ny=0, ny1=-1) + elif self._colorbar_location == "bottom": + #sz = Size.Fraction(Size.AxesY(self.axes_llc), self._ncols) + sz = Size.Fraction(self._ncols, Size.AxesY(self.axes_llc)) + v.append(Size.from_any(self._colorbar_size, sz)) + v.append(Size.from_any(self._colorbar_pad, sz)) + locator = self._divider.new_locator(nx=0, nx1=-1, ny=0) + for i in range(self.ngrids): + self.cbar_axes[i].set_visible(False) + self.cbar_axes[0].set_axes_locator(locator) + self.cbar_axes[0].set_visible(True) + + for col, ax in enumerate(self.axes_row[0]): + if h: + h.append(self._horiz_pad_size) # Size.Fixed(self._axes_pad)) + + if ax: + sz = Size.AxesX(ax, aspect="axes", ref_ax=self.axes_all[0]) + else: + sz = Size.AxesX(self.axes_all[0], + aspect="axes", ref_ax=self.axes_all[0]) + + if (self._colorbar_mode == "each" or + (self._colorbar_mode == 'edge' and + col == 0)) and self._colorbar_location == "left": + h_cb_pos.append(len(h)) + h.append(Size.from_any(self._colorbar_size, sz)) + h.append(Size.from_any(self._colorbar_pad, sz)) + + h_ax_pos.append(len(h)) + + h.append(sz) + + if ((self._colorbar_mode == "each" or + (self._colorbar_mode == 'edge' and + col == self._ncols - 1)) and + self._colorbar_location == "right"): + h.append(Size.from_any(self._colorbar_pad, sz)) + h_cb_pos.append(len(h)) + h.append(Size.from_any(self._colorbar_size, sz)) + + v_ax_pos = [] + v_cb_pos = [] + for row, ax in enumerate(self.axes_column[0][::-1]): + if v: + v.append(self._vert_pad_size) # Size.Fixed(self._axes_pad)) + + if ax: + sz = Size.AxesY(ax, aspect="axes", ref_ax=self.axes_all[0]) + else: + sz = Size.AxesY(self.axes_all[0], + aspect="axes", ref_ax=self.axes_all[0]) + + if (self._colorbar_mode == "each" or + (self._colorbar_mode == 'edge' and + row == 0)) and self._colorbar_location == "bottom": + v_cb_pos.append(len(v)) + v.append(Size.from_any(self._colorbar_size, sz)) + v.append(Size.from_any(self._colorbar_pad, sz)) + + v_ax_pos.append(len(v)) + v.append(sz) + + if ((self._colorbar_mode == "each" or + (self._colorbar_mode == 'edge' and + row == self._nrows - 1)) and + self._colorbar_location == "top"): + v.append(Size.from_any(self._colorbar_pad, sz)) + v_cb_pos.append(len(v)) + v.append(Size.from_any(self._colorbar_size, sz)) + + for i in range(self.ngrids): + col, row = self._get_col_row(i) + #locator = self._divider.new_locator(nx=4*col, + # ny=2*(self._nrows - row - 1)) + locator = self._divider.new_locator(nx=h_ax_pos[col], + ny=v_ax_pos[self._nrows-1-row]) + self.axes_all[i].set_axes_locator(locator) + + if self._colorbar_mode == "each": + if self._colorbar_location in ("right", "left"): + locator = self._divider.new_locator( + nx=h_cb_pos[col], ny=v_ax_pos[self._nrows - 1 - row]) + + elif self._colorbar_location in ("top", "bottom"): + locator = self._divider.new_locator( + nx=h_ax_pos[col], ny=v_cb_pos[self._nrows - 1 - row]) + + self.cbar_axes[i].set_axes_locator(locator) + elif self._colorbar_mode == 'edge': + if ((self._colorbar_location == 'left' and col == 0) or + (self._colorbar_location == 'right' + and col == self._ncols-1)): + locator = self._divider.new_locator( + nx=h_cb_pos[0], ny=v_ax_pos[self._nrows -1 - row]) + self.cbar_axes[row].set_axes_locator(locator) + elif ((self._colorbar_location == 'bottom' and + row == self._nrows - 1) or + (self._colorbar_location == 'top' and row == 0)): + locator = self._divider.new_locator(nx=h_ax_pos[col], + ny=v_cb_pos[0]) + self.cbar_axes[col].set_axes_locator(locator) + + if self._colorbar_mode == "single": + if self._colorbar_location == "right": + #sz = Size.Fraction(Size.AxesX(self.axes_llc), self._nrows) + sz = Size.Fraction(self._nrows, Size.AxesX(self.axes_llc)) + h.append(Size.from_any(self._colorbar_pad, sz)) + h.append(Size.from_any(self._colorbar_size, sz)) + locator = self._divider.new_locator(nx=-2, ny=0, ny1=-1) + elif self._colorbar_location == "top": + #sz = Size.Fraction(Size.AxesY(self.axes_llc), self._ncols) + sz = Size.Fraction(self._ncols, Size.AxesY(self.axes_llc)) + v.append(Size.from_any(self._colorbar_pad, sz)) + v.append(Size.from_any(self._colorbar_size, sz)) + locator = self._divider.new_locator(nx=0, nx1=-1, ny=-2) + if self._colorbar_location in ("right", "top"): + for i in range(self.ngrids): + self.cbar_axes[i].set_visible(False) + self.cbar_axes[0].set_axes_locator(locator) + self.cbar_axes[0].set_visible(True) + elif self._colorbar_mode == "each": + for i in range(self.ngrids): + self.cbar_axes[i].set_visible(True) + elif self._colorbar_mode == "edge": + if self._colorbar_location in ('right', 'left'): + count = self._nrows + else: + count = self._ncols + for i in range(count): + self.cbar_axes[i].set_visible(True) + for j in range(i + 1, self.ngrids): + self.cbar_axes[j].set_visible(False) + else: + for i in range(self.ngrids): + self.cbar_axes[i].set_visible(False) + self.cbar_axes[i].set_position([1., 1., 0.001, 0.001], + which="active") + + self._divider.set_horizontal(h) + self._divider.set_vertical(v) + + +AxesGrid = ImageGrid + diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/axes_rgb.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/axes_rgb.py new file mode 100644 index 0000000000..e62d4f0615 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/axes_rgb.py @@ -0,0 +1,228 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six + +import numpy as np +from .axes_divider import make_axes_locatable, Size, locatable_axes_factory +import sys +from .mpl_axes import Axes + + +def make_rgb_axes(ax, pad=0.01, axes_class=None, add_all=True): + """ + pad : fraction of the axes height. + """ + + divider = make_axes_locatable(ax) + + pad_size = Size.Fraction(pad, Size.AxesY(ax)) + + xsize = Size.Fraction((1.-2.*pad)/3., Size.AxesX(ax)) + ysize = Size.Fraction((1.-2.*pad)/3., Size.AxesY(ax)) + + divider.set_horizontal([Size.AxesX(ax), pad_size, xsize]) + divider.set_vertical([ysize, pad_size, ysize, pad_size, ysize]) + + ax.set_axes_locator(divider.new_locator(0, 0, ny1=-1)) + + ax_rgb = [] + if axes_class is None: + try: + axes_class = locatable_axes_factory(ax._axes_class) + except AttributeError: + axes_class = locatable_axes_factory(type(ax)) + + for ny in [4, 2, 0]: + ax1 = axes_class(ax.get_figure(), + ax.get_position(original=True), + sharex=ax, sharey=ax) + locator = divider.new_locator(nx=2, ny=ny) + ax1.set_axes_locator(locator) + for t in ax1.yaxis.get_ticklabels() + ax1.xaxis.get_ticklabels(): + t.set_visible(False) + try: + for axis in ax1.axis.values(): + axis.major_ticklabels.set_visible(False) + except AttributeError: + pass + + ax_rgb.append(ax1) + + if add_all: + fig = ax.get_figure() + for ax1 in ax_rgb: + fig.add_axes(ax1) + + return ax_rgb + + +def imshow_rgb(ax, r, g, b, **kwargs): + ny, nx = r.shape + R = np.zeros([ny, nx, 3], dtype="d") + R[:,:,0] = r + G = np.zeros_like(R) + G[:,:,1] = g + B = np.zeros_like(R) + B[:,:,2] = b + + RGB = R + G + B + + im_rgb = ax.imshow(RGB, **kwargs) + + return im_rgb + + +class RGBAxesBase(object): + """base class for a 4-panel imshow (RGB, R, G, B) + + Layout: + +---------------+-----+ + | | R | + + +-----+ + | RGB | G | + + +-----+ + | | B | + +---------------+-----+ + + Attributes + ---------- + _defaultAxesClass : matplotlib.axes.Axes + defaults to 'Axes' in RGBAxes child class. + No default in abstract base class + RGB : _defaultAxesClass + The axes object for the three-channel imshow + R : _defaultAxesClass + The axes object for the red channel imshow + G : _defaultAxesClass + The axes object for the green channel imshow + B : _defaultAxesClass + The axes object for the blue channel imshow + """ + def __init__(self, *kl, **kwargs): + """ + Parameters + ---------- + pad : float + fraction of the axes height to put as padding. + defaults to 0.0 + add_all : bool + True: Add the {rgb, r, g, b} axes to the figure + defaults to True. + axes_class : matplotlib.axes.Axes + + kl : + Unpacked into axes_class() init for RGB + kwargs : + Unpacked into axes_class() init for RGB, R, G, B axes + """ + pad = kwargs.pop("pad", 0.0) + add_all = kwargs.pop("add_all", True) + try: + axes_class = kwargs.pop("axes_class", self._defaultAxesClass) + except AttributeError: + new_msg = ("A subclass of RGBAxesBase must have a " + "_defaultAxesClass attribute. If you are not sure which " + "axes class to use, consider using " + "mpl_toolkits.axes_grid1.mpl_axes.Axes.") + six.reraise(AttributeError, AttributeError(new_msg), + sys.exc_info()[2]) + + ax = axes_class(*kl, **kwargs) + + divider = make_axes_locatable(ax) + + pad_size = Size.Fraction(pad, Size.AxesY(ax)) + + xsize = Size.Fraction((1.-2.*pad)/3., Size.AxesX(ax)) + ysize = Size.Fraction((1.-2.*pad)/3., Size.AxesY(ax)) + + divider.set_horizontal([Size.AxesX(ax), pad_size, xsize]) + divider.set_vertical([ysize, pad_size, ysize, pad_size, ysize]) + + ax.set_axes_locator(divider.new_locator(0, 0, ny1=-1)) + + ax_rgb = [] + for ny in [4, 2, 0]: + ax1 = axes_class(ax.get_figure(), + ax.get_position(original=True), + sharex=ax, sharey=ax, **kwargs) + locator = divider.new_locator(nx=2, ny=ny) + ax1.set_axes_locator(locator) + ax1.axis[:].toggle(ticklabels=False) + ax_rgb.append(ax1) + + self.RGB = ax + self.R, self.G, self.B = ax_rgb + + if add_all: + fig = ax.get_figure() + fig.add_axes(ax) + self.add_RGB_to_figure() + + self._config_axes() + + def _config_axes(self, line_color='w', marker_edge_color='w'): + """Set the line color and ticks for the axes + + Parameters + ---------- + line_color : any matplotlib color + marker_edge_color : any matplotlib color + """ + for ax1 in [self.RGB, self.R, self.G, self.B]: + ax1.axis[:].line.set_color(line_color) + ax1.axis[:].major_ticks.set_markeredgecolor(marker_edge_color) + + def add_RGB_to_figure(self): + """Add the red, green and blue axes to the RGB composite's axes figure + """ + self.RGB.get_figure().add_axes(self.R) + self.RGB.get_figure().add_axes(self.G) + self.RGB.get_figure().add_axes(self.B) + + def imshow_rgb(self, r, g, b, **kwargs): + """Create the four images {rgb, r, g, b} + + Parameters + ---------- + r : array-like + The red array + g : array-like + The green array + b : array-like + The blue array + kwargs : imshow kwargs + kwargs get unpacked into the imshow calls for the four images + + Returns + ------- + rgb : matplotlib.image.AxesImage + r : matplotlib.image.AxesImage + g : matplotlib.image.AxesImage + b : matplotlib.image.AxesImage + """ + if not (r.shape == g.shape == b.shape): + raise ValueError('Input shapes do not match.' + '\nr.shape = {}' + '\ng.shape = {}' + '\nb.shape = {}' + .format(r.shape, g.shape, b.shape)) + RGB = np.dstack([r, g, b]) + R = np.zeros_like(RGB) + R[:,:,0] = r + G = np.zeros_like(RGB) + G[:,:,1] = g + B = np.zeros_like(RGB) + B[:,:,2] = b + + im_rgb = self.RGB.imshow(RGB, **kwargs) + im_r = self.R.imshow(R, **kwargs) + im_g = self.G.imshow(G, **kwargs) + im_b = self.B.imshow(B, **kwargs) + + return im_rgb, im_r, im_g, im_b + + +class RGBAxes(RGBAxesBase): + _defaultAxesClass = Axes diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/axes_size.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/axes_size.py new file mode 100644 index 0000000000..163a6245fe --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/axes_size.py @@ -0,0 +1,323 @@ + +""" +provides a classes of simple units that will be used with AxesDivider +class (or others) to determine the size of each axes. The unit +classes define `get_size` method that returns a tuple of two floats, +meaning relative and absolute sizes, respectively. + +Note that this class is nothing more than a simple tuple of two +floats. Take a look at the Divider class to see how these two +values are used. + +""" +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six + +import matplotlib.cbook as cbook +from matplotlib.axes import Axes + +class _Base(object): + "Base class" + + def __rmul__(self, other): + float(other) # just to check if number if given + return Fraction(other, self) + + def __add__(self, other): + if isinstance(other, _Base): + return Add(self, other) + else: + float(other) + other = Fixed(other) + return Add(self, other) + + +class Add(_Base): + def __init__(self, a, b): + self._a = a + self._b = b + + def get_size(self, renderer): + a_rel_size, a_abs_size = self._a.get_size(renderer) + b_rel_size, b_abs_size = self._b.get_size(renderer) + return a_rel_size + b_rel_size, a_abs_size + b_abs_size + +class AddList(_Base): + def __init__(self, add_list): + self._list = add_list + + def get_size(self, renderer): + sum_rel_size = sum([a.get_size(renderer)[0] for a in self._list]) + sum_abs_size = sum([a.get_size(renderer)[1] for a in self._list]) + return sum_rel_size, sum_abs_size + + +class Fixed(_Base): + "Simple fixed size with absolute part = *fixed_size* and relative part = 0" + def __init__(self, fixed_size): + self.fixed_size = fixed_size + + def get_size(self, renderer): + rel_size = 0. + abs_size = self.fixed_size + return rel_size, abs_size + + +class Scaled(_Base): + "Simple scaled(?) size with absolute part = 0 and relative part = *scalable_size*" + def __init__(self, scalable_size): + self._scalable_size = scalable_size + + def get_size(self, renderer): + rel_size = self._scalable_size + abs_size = 0. + return rel_size, abs_size + +Scalable=Scaled + +def _get_axes_aspect(ax): + aspect = ax.get_aspect() + # when aspec is "auto", consider it as 1. + if aspect in ('normal', 'auto'): + aspect = 1. + elif aspect == "equal": + aspect = 1 + else: + aspect = float(aspect) + + return aspect + +class AxesX(_Base): + """ + Scaled size whose relative part corresponds to the data width + of the *axes* multiplied by the *aspect*. + """ + def __init__(self, axes, aspect=1., ref_ax=None): + self._axes = axes + self._aspect = aspect + if aspect == "axes" and ref_ax is None: + raise ValueError("ref_ax must be set when aspect='axes'") + self._ref_ax = ref_ax + + def get_size(self, renderer): + l1, l2 = self._axes.get_xlim() + if self._aspect == "axes": + ref_aspect = _get_axes_aspect(self._ref_ax) + aspect = ref_aspect/_get_axes_aspect(self._axes) + else: + aspect = self._aspect + + rel_size = abs(l2-l1)*aspect + abs_size = 0. + return rel_size, abs_size + +class AxesY(_Base): + """ + Scaled size whose relative part corresponds to the data height + of the *axes* multiplied by the *aspect*. + """ + def __init__(self, axes, aspect=1., ref_ax=None): + self._axes = axes + self._aspect = aspect + if aspect == "axes" and ref_ax is None: + raise ValueError("ref_ax must be set when aspect='axes'") + self._ref_ax = ref_ax + + def get_size(self, renderer): + l1, l2 = self._axes.get_ylim() + + if self._aspect == "axes": + ref_aspect = _get_axes_aspect(self._ref_ax) + aspect = _get_axes_aspect(self._axes) + else: + aspect = self._aspect + + rel_size = abs(l2-l1)*aspect + abs_size = 0. + return rel_size, abs_size + + +class MaxExtent(_Base): + """ + Size whose absolute part is the largest width (or height) of + the given *artist_list*. + """ + def __init__(self, artist_list, w_or_h): + self._artist_list = artist_list + + if w_or_h not in ["width", "height"]: + raise ValueError() + + self._w_or_h = w_or_h + + def add_artist(self, a): + self._artist_list.append(a) + + def get_size(self, renderer): + rel_size = 0. + w_list, h_list = [], [] + for a in self._artist_list: + bb = a.get_window_extent(renderer) + w_list.append(bb.width) + h_list.append(bb.height) + dpi = a.get_figure().get_dpi() + if self._w_or_h == "width": + abs_size = max(w_list)/dpi + elif self._w_or_h == "height": + abs_size = max(h_list)/dpi + + return rel_size, abs_size + + +class MaxWidth(_Base): + """ + Size whose absolute part is the largest width of + the given *artist_list*. + """ + def __init__(self, artist_list): + self._artist_list = artist_list + + def add_artist(self, a): + self._artist_list.append(a) + + def get_size(self, renderer): + rel_size = 0. + w_list = [] + for a in self._artist_list: + bb = a.get_window_extent(renderer) + w_list.append(bb.width) + dpi = a.get_figure().get_dpi() + abs_size = max(w_list)/dpi + + return rel_size, abs_size + + + +class MaxHeight(_Base): + """ + Size whose absolute part is the largest height of + the given *artist_list*. + """ + def __init__(self, artist_list): + self._artist_list = artist_list + + def add_artist(self, a): + self._artist_list.append(a) + + def get_size(self, renderer): + rel_size = 0. + h_list = [] + for a in self._artist_list: + bb = a.get_window_extent(renderer) + h_list.append(bb.height) + dpi = a.get_figure().get_dpi() + abs_size = max(h_list)/dpi + + return rel_size, abs_size + + +class Fraction(_Base): + """ + An instance whose size is a *fraction* of the *ref_size*. + :: + + >>> s = Fraction(0.3, AxesX(ax)) + + """ + def __init__(self, fraction, ref_size): + self._fraction_ref = ref_size + self._fraction = fraction + + def get_size(self, renderer): + if self._fraction_ref is None: + return self._fraction, 0. + else: + r, a = self._fraction_ref.get_size(renderer) + rel_size = r*self._fraction + abs_size = a*self._fraction + return rel_size, abs_size + +class Padded(_Base): + """ + Return a instance where the absolute part of *size* is + increase by the amount of *pad*. + """ + def __init__(self, size, pad): + self._size = size + self._pad = pad + + def get_size(self, renderer): + r, a = self._size.get_size(renderer) + rel_size = r + abs_size = a + self._pad + return rel_size, abs_size + +def from_any(size, fraction_ref=None): + """ + Creates Fixed unit when the first argument is a float, or a + Fraction unit if that is a string that ends with %. The second + argument is only meaningful when Fraction unit is created.:: + + >>> a = Size.from_any(1.2) # => Size.Fixed(1.2) + >>> Size.from_any("50%", a) # => Size.Fraction(0.5, a) + + """ + if cbook.is_numlike(size): + return Fixed(size) + elif isinstance(size, six.string_types): + if size[-1] == "%": + return Fraction(float(size[:-1]) / 100, fraction_ref) + + raise ValueError("Unknown format") + + +class SizeFromFunc(_Base): + def __init__(self, func): + self._func = func + + def get_size(self, renderer): + rel_size = 0. + + bb = self._func(renderer) + dpi = renderer.points_to_pixels(72.) + abs_size = bb/dpi + + return rel_size, abs_size + +class GetExtentHelper(object): + def _get_left(tight_bbox, axes_bbox): + return axes_bbox.xmin - tight_bbox.xmin + + def _get_right(tight_bbox, axes_bbox): + return tight_bbox.xmax - axes_bbox.xmax + + def _get_bottom(tight_bbox, axes_bbox): + return axes_bbox.ymin - tight_bbox.ymin + + def _get_top(tight_bbox, axes_bbox): + return tight_bbox.ymax - axes_bbox.ymax + + _get_func_map = dict(left=_get_left, + right=_get_right, + bottom=_get_bottom, + top=_get_top) + + del _get_left, _get_right, _get_bottom, _get_top + + def __init__(self, ax, direction): + if isinstance(ax, Axes): + self._ax_list = [ax] + else: + self._ax_list = ax + + try: + self._get_func = self._get_func_map[direction] + except KeyError: + raise KeyError("direction must be one of left, right, bottom, top") + + def __call__(self, renderer): + vl = [self._get_func(ax.get_tightbbox(renderer, False), + ax.bbox) for ax in self._ax_list] + return max(vl) diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/colorbar.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/colorbar.py new file mode 100644 index 0000000000..34bdf3618a --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/colorbar.py @@ -0,0 +1,836 @@ +""" +Colorbar toolkit with two classes and a function: + + :class:`ColorbarBase` + the base class with full colorbar drawing functionality. + It can be used as-is to make a colorbar for a given colormap; + a mappable object (e.g., image) is not needed. + + :class:`Colorbar` + the derived class for use with images or contour plots. + + :func:`make_axes` + a function for resizing an axes and adding a second axes + suitable for a colorbar + +The :meth:`~matplotlib.figure.Figure.colorbar` method uses :func:`make_axes` +and :class:`Colorbar`; the :func:`~matplotlib.pyplot.colorbar` function +is a thin wrapper over :meth:`~matplotlib.figure.Figure.colorbar`. +""" + +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six +from six.moves import xrange, zip + +import numpy as np +import matplotlib as mpl +import matplotlib.colors as colors +import matplotlib.cm as cm +from matplotlib import docstring +import matplotlib.ticker as ticker +import matplotlib.cbook as cbook +import matplotlib.collections as collections +import matplotlib.contour as contour +from matplotlib.path import Path +from matplotlib.patches import PathPatch +from matplotlib.transforms import Bbox + + +make_axes_kw_doc = ''' + + ============= ==================================================== + Property Description + ============= ==================================================== + *orientation* vertical or horizontal + *fraction* 0.15; fraction of original axes to use for colorbar + *pad* 0.05 if vertical, 0.15 if horizontal; fraction + of original axes between colorbar and new image axes + *shrink* 1.0; fraction by which to shrink the colorbar + *aspect* 20; ratio of long to short dimensions + ============= ==================================================== + +''' + +colormap_kw_doc = ''' + + =========== ==================================================== + Property Description + =========== ==================================================== + *extend* [ 'neither' | 'both' | 'min' | 'max' ] + If not 'neither', make pointed end(s) for out-of- + range values. These are set for a given colormap + using the colormap set_under and set_over methods. + *spacing* [ 'uniform' | 'proportional' ] + Uniform spacing gives each discrete color the same + space; proportional makes the space proportional to + the data interval. + *ticks* [ None | list of ticks | Locator object ] + If None, ticks are determined automatically from the + input. + *format* [ None | format string | Formatter object ] + If None, the + :class:`~matplotlib.ticker.ScalarFormatter` is used. + If a format string is given, e.g., '%.3f', that is + used. An alternative + :class:`~matplotlib.ticker.Formatter` object may be + given instead. + *drawedges* bool + Whether to draw lines at color boundaries. + =========== ==================================================== + + The following will probably be useful only in the context of + indexed colors (that is, when the mappable has norm=NoNorm()), + or other unusual circumstances. + + ============ =================================================== + Property Description + ============ =================================================== + *boundaries* None or a sequence + *values* None or a sequence which must be of length 1 less + than the sequence of *boundaries*. For each region + delimited by adjacent entries in *boundaries*, the + color mapped to the corresponding value in values + will be used. + ============ =================================================== + +''' + +colorbar_doc = ''' + +Add a colorbar to a plot. + +Function signatures for the :mod:`~matplotlib.pyplot` interface; all +but the first are also method signatures for the +:meth:`~matplotlib.figure.Figure.colorbar` method:: + + colorbar(**kwargs) + colorbar(mappable, **kwargs) + colorbar(mappable, cax=cax, **kwargs) + colorbar(mappable, ax=ax, **kwargs) + +arguments: + + *mappable* + the :class:`~matplotlib.image.Image`, + :class:`~matplotlib.contour.ContourSet`, etc. to + which the colorbar applies; this argument is mandatory for the + :meth:`~matplotlib.figure.Figure.colorbar` method but optional for the + :func:`~matplotlib.pyplot.colorbar` function, which sets the + default to the current image. + +keyword arguments: + + *cax* + None | axes object into which the colorbar will be drawn + *ax* + None | parent axes object from which space for a new + colorbar axes will be stolen + + +Additional keyword arguments are of two kinds: + + axes properties: + %s + colorbar properties: + %s + +If *mappable* is a :class:`~matplotlib.contours.ContourSet`, its *extend* +kwarg is included automatically. + +Note that the *shrink* kwarg provides a simple way to keep a vertical +colorbar, for example, from being taller than the axes of the mappable +to which the colorbar is attached; but it is a manual method requiring +some trial and error. If the colorbar is too tall (or a horizontal +colorbar is too wide) use a smaller value of *shrink*. + +For more precise control, you can manually specify the positions of +the axes objects in which the mappable and the colorbar are drawn. In +this case, do not use any of the axes properties kwargs. + +It is known that some vector graphics viewer (svg and pdf) renders white gaps +between segments of the colorbar. This is due to bugs in the viewers not +matplotlib. As a workaround the colorbar can be rendered with overlapping +segments:: + + cbar = colorbar() + cbar.solids.set_edgecolor("face") + draw() + +However this has negative consequences in other circumstances. Particularly with +semi transparent images (alpha < 1) and colorbar extensions and is not enabled +by default see (issue #1188). + +returns: + :class:`~matplotlib.colorbar.Colorbar` instance; see also its base class, + :class:`~matplotlib.colorbar.ColorbarBase`. Call the + :meth:`~matplotlib.colorbar.ColorbarBase.set_label` method + to label the colorbar. + + +The transData of the *cax* is adjusted so that the limits in the +longest axis actually corresponds to the limits in colorbar range. On +the other hand, the shortest axis has a data limits of [1,2], whose +unconventional value is to prevent underflow when log scale is used. +''' % (make_axes_kw_doc, colormap_kw_doc) + +#docstring.interpd.update(colorbar_doc=colorbar_doc) + + +class CbarAxesLocator(object): + """ + CbarAxesLocator is a axes_locator for colorbar axes. It adjust the + position of the axes to make a room for extended ends, i.e., the + extended ends are located outside the axes area. + """ + + def __init__(self, locator=None, extend="neither", orientation="vertical"): + """ + *locator* : the bbox returned from the locator is used as a + initial axes location. If None, axes.bbox is used. + + *extend* : same as in ColorbarBase + *orientation* : same as in ColorbarBase + + """ + self._locator = locator + self.extesion_fraction = 0.05 + self.extend = extend + self.orientation = orientation + + def get_original_position(self, axes, renderer): + """ + get the original position of the axes. + """ + if self._locator is None: + bbox = axes.get_position(original=True) + else: + bbox = self._locator(axes, renderer) + return bbox + + def get_end_vertices(self): + """ + return a tuple of two vertices for the colorbar extended ends. + The first vertices is for the minimum end, and the second is for + the maximum end. + """ + # Note that concatenating two vertices needs to make a + # vertices for the frame. + extesion_fraction = self.extesion_fraction + + corx = extesion_fraction*2. + cory = 1./(1. - corx) + x1, y1, w, h = 0, 0, 1, 1 + x2, y2 = x1 + w, y1 + h + dw, dh = w*extesion_fraction, h*extesion_fraction*cory + + if self.extend in ["min", "both"]: + bottom = [(x1, y1), + (x1+w/2., y1-dh), + (x2, y1)] + else: + bottom = [(x1, y1), + (x2, y1)] + + if self.extend in ["max", "both"]: + top = [(x2, y2), + (x1+w/2., y2+dh), + (x1, y2)] + else: + top = [(x2, y2), + (x1, y2)] + + if self.orientation == "horizontal": + bottom = [(y,x) for (x,y) in bottom] + top = [(y,x) for (x,y) in top] + + return bottom, top + + + def get_path_patch(self): + """ + get the path for axes patch + """ + end1, end2 = self.get_end_vertices() + + verts = [] + end1 + end2 + end1[:1] + + return Path(verts) + + + def get_path_ends(self): + """ + get the paths for extended ends + """ + + end1, end2 = self.get_end_vertices() + + return Path(end1), Path(end2) + + + def __call__(self, axes, renderer): + """ + Return the adjusted position of the axes + """ + bbox0 = self.get_original_position(axes, renderer) + bbox = bbox0 + + x1, y1, w, h = bbox.bounds + extesion_fraction = self.extesion_fraction + dw, dh = w*extesion_fraction, h*extesion_fraction + + if self.extend in ["min", "both"]: + if self.orientation == "horizontal": + x1 = x1 + dw + else: + y1 = y1+dh + + if self.extend in ["max", "both"]: + if self.orientation == "horizontal": + w = w-2*dw + else: + h = h-2*dh + + return Bbox.from_bounds(x1, y1, w, h) + + + +class ColorbarBase(cm.ScalarMappable): + ''' + Draw a colorbar in an existing axes. + + This is a base class for the :class:`Colorbar` class, which is the + basis for the :func:`~matplotlib.pyplot.colorbar` method and pylab + function. + + It is also useful by itself for showing a colormap. If the *cmap* + kwarg is given but *boundaries* and *values* are left as None, + then the colormap will be displayed on a 0-1 scale. To show the + under- and over-value colors, specify the *norm* as:: + + colors.Normalize(clip=False) + + To show the colors versus index instead of on the 0-1 scale, + use:: + + norm=colors.NoNorm. + + Useful attributes: + + :attr:`ax` + the Axes instance in which the colorbar is drawn + + :attr:`lines` + a LineCollection if lines were drawn, otherwise None + + :attr:`dividers` + a LineCollection if *drawedges* is True, otherwise None + + Useful public methods are :meth:`set_label` and :meth:`add_lines`. + + ''' + + def __init__(self, ax, cmap=None, + norm=None, + alpha=1.0, + values=None, + boundaries=None, + orientation='vertical', + extend='neither', + spacing='uniform', # uniform or proportional + ticks=None, + format=None, + drawedges=False, + filled=True, + ): + self.ax = ax + + if cmap is None: cmap = cm.get_cmap() + if norm is None: norm = colors.Normalize() + self.alpha = alpha + cm.ScalarMappable.__init__(self, cmap=cmap, norm=norm) + self.values = values + self.boundaries = boundaries + self.extend = extend + self.spacing = spacing + self.orientation = orientation + self.drawedges = drawedges + self.filled = filled + + # artists + self.solids = None + self.lines = None + self.dividers = None + self.extension_patch1 = None + self.extension_patch2 = None + + if orientation == "vertical": + self.cbar_axis = self.ax.yaxis + else: + self.cbar_axis = self.ax.xaxis + + + if format is None: + if isinstance(self.norm, colors.LogNorm): + # change both axis for proper aspect + self.ax.set_xscale("log") + self.ax.set_yscale("log") + self.cbar_axis.set_minor_locator(ticker.NullLocator()) + formatter = ticker.LogFormatter() + else: + formatter = None + elif isinstance(format, six.string_types): + formatter = ticker.FormatStrFormatter(format) + else: + formatter = format # Assume it is a Formatter + + if formatter is None: + formatter = self.cbar_axis.get_major_formatter() + else: + self.cbar_axis.set_major_formatter(formatter) + + if cbook.iterable(ticks): + self.cbar_axis.set_ticks(ticks) + elif ticks is not None: + self.cbar_axis.set_major_locator(ticks) + else: + self._select_locator(formatter) + + + self._config_axes() + + self.update_artists() + + self.set_label_text('') + + + def _get_colorbar_limits(self): + """ + initial limits for colorbar range. The returned min, max values + will be used to create colorbar solid(?) and etc. + """ + if self.boundaries is not None: + C = self.boundaries + if self.extend in ["min", "both"]: + C = C[1:] + + if self.extend in ["max", "both"]: + C = C[:-1] + return min(C), max(C) + else: + return self.get_clim() + + + def _config_axes(self): + ''' + Adjust the properties of the axes to be adequate for colorbar display. + ''' + ax = self.ax + + axes_locator = CbarAxesLocator(ax.get_axes_locator(), + extend=self.extend, + orientation=self.orientation) + ax.set_axes_locator(axes_locator) + + # override the get_data_ratio for the aspect works. + def _f(): + return 1. + ax.get_data_ratio = _f + ax.get_data_ratio_log = _f + + ax.set_frame_on(True) + ax.set_navigate(False) + + self.ax.set_autoscalex_on(False) + self.ax.set_autoscaley_on(False) + + if self.orientation == 'horizontal': + ax.xaxis.set_label_position('bottom') + ax.set_yticks([]) + else: + ax.set_xticks([]) + ax.yaxis.set_label_position('right') + ax.yaxis.set_ticks_position('right') + + + + def update_artists(self): + """ + Update the colorbar associated artists, *filled* and + *ends*. Note that *lines* are not updated. This needs to be + called whenever clim of associated image changes. + """ + self._process_values() + self._add_ends() + + X, Y = self._mesh() + if self.filled: + C = self._values[:,np.newaxis] + self._add_solids(X, Y, C) + + ax = self.ax + vmin, vmax = self._get_colorbar_limits() + if self.orientation == 'horizontal': + ax.set_ylim(1, 2) + ax.set_xlim(vmin, vmax) + else: + ax.set_xlim(1, 2) + ax.set_ylim(vmin, vmax) + + + def _add_ends(self): + """ + Create patches from extended ends and add them to the axes. + """ + + del self.extension_patch1 + del self.extension_patch2 + + path1, path2 = self.ax.get_axes_locator().get_path_ends() + fc=mpl.rcParams['axes.facecolor'] + ec=mpl.rcParams['axes.edgecolor'] + linewidths=0.5*mpl.rcParams['axes.linewidth'] + self.extension_patch1 = PathPatch(path1, + fc=fc, ec=ec, lw=linewidths, + zorder=2., + transform=self.ax.transAxes, + clip_on=False) + self.extension_patch2 = PathPatch(path2, + fc=fc, ec=ec, lw=linewidths, + zorder=2., + transform=self.ax.transAxes, + clip_on=False) + self.ax.add_artist(self.extension_patch1) + self.ax.add_artist(self.extension_patch2) + + + + def _set_label_text(self): + """ + set label. + """ + self.cbar_axis.set_label_text(self._label, **self._labelkw) + + def set_label_text(self, label, **kw): + ''' + Label the long axis of the colorbar + ''' + self._label = label + self._labelkw = kw + self._set_label_text() + + + def _edges(self, X, Y): + ''' + Return the separator line segments; helper for _add_solids. + ''' + N = X.shape[0] + # Using the non-array form of these line segments is much + # simpler than making them into arrays. + if self.orientation == 'vertical': + return [list(zip(X[i], Y[i])) for i in xrange(1, N-1)] + else: + return [list(zip(Y[i], X[i])) for i in xrange(1, N-1)] + + def _add_solids(self, X, Y, C): + ''' + Draw the colors using :meth:`~matplotlib.axes.Axes.pcolormesh`; + optionally add separators. + ''' + ## Change to pcolorfast after fixing bugs in some backends... + + if self.extend in ["min", "both"]: + cc = self.to_rgba([C[0][0]]) + self.extension_patch1.set_fc(cc[0]) + X, Y, C = X[1:], Y[1:], C[1:] + + if self.extend in ["max", "both"]: + cc = self.to_rgba([C[-1][0]]) + self.extension_patch2.set_fc(cc[0]) + X, Y, C = X[:-1], Y[:-1], C[:-1] + + if self.orientation == 'vertical': + args = (X, Y, C) + else: + args = (np.transpose(Y), np.transpose(X), np.transpose(C)) + kw = {'cmap':self.cmap, 'norm':self.norm, + 'shading':'flat', 'alpha':self.alpha, + } + + del self.solids + del self.dividers + + col = self.ax.pcolormesh(*args, **kw) + + self.solids = col + if self.drawedges: + self.dividers = collections.LineCollection(self._edges(X,Y), + colors=(mpl.rcParams['axes.edgecolor'],), + linewidths=(0.5*mpl.rcParams['axes.linewidth'],), + ) + self.ax.add_collection(self.dividers) + else: + self.dividers = None + + def add_lines(self, levels, colors, linewidths): + ''' + Draw lines on the colorbar. It deletes preexisting lines. + ''' + del self.lines + + N = len(levels) + x = np.array([1.0, 2.0]) + X, Y = np.meshgrid(x,levels) + if self.orientation == 'vertical': + xy = [list(zip(X[i], Y[i])) for i in xrange(N)] + else: + xy = [list(zip(Y[i], X[i])) for i in xrange(N)] + col = collections.LineCollection(xy, linewidths=linewidths, + ) + self.lines = col + col.set_color(colors) + self.ax.add_collection(col) + + + def _select_locator(self, formatter): + ''' + select a suitable locator + ''' + if self.boundaries is None: + if isinstance(self.norm, colors.NoNorm): + nv = len(self._values) + base = 1 + int(nv/10) + locator = ticker.IndexLocator(base=base, offset=0) + elif isinstance(self.norm, colors.BoundaryNorm): + b = self.norm.boundaries + locator = ticker.FixedLocator(b, nbins=10) + elif isinstance(self.norm, colors.LogNorm): + locator = ticker.LogLocator() + else: + locator = ticker.MaxNLocator(nbins=5) + else: + b = self._boundaries[self._inside] + locator = ticker.FixedLocator(b) #, nbins=10) + + self.cbar_axis.set_major_locator(locator) + + + def _process_values(self, b=None): + ''' + Set the :attr:`_boundaries` and :attr:`_values` attributes + based on the input boundaries and values. Input boundaries + can be *self.boundaries* or the argument *b*. + ''' + if b is None: + b = self.boundaries + if b is not None: + self._boundaries = np.asarray(b, dtype=float) + if self.values is None: + self._values = 0.5*(self._boundaries[:-1] + + self._boundaries[1:]) + if isinstance(self.norm, colors.NoNorm): + self._values = (self._values + 0.00001).astype(np.int16) + return + self._values = np.array(self.values) + return + if self.values is not None: + self._values = np.array(self.values) + if self.boundaries is None: + b = np.zeros(len(self.values)+1, 'd') + b[1:-1] = 0.5*(self._values[:-1] - self._values[1:]) + b[0] = 2.0*b[1] - b[2] + b[-1] = 2.0*b[-2] - b[-3] + self._boundaries = b + return + self._boundaries = np.array(self.boundaries) + return + # Neither boundaries nor values are specified; + # make reasonable ones based on cmap and norm. + if isinstance(self.norm, colors.NoNorm): + b = self._uniform_y(self.cmap.N+1) * self.cmap.N - 0.5 + v = np.zeros((len(b)-1,), dtype=np.int16) + v = np.arange(self.cmap.N, dtype=np.int16) + self._boundaries = b + self._values = v + return + elif isinstance(self.norm, colors.BoundaryNorm): + b = np.array(self.norm.boundaries) + v = np.zeros((len(b)-1,), dtype=float) + bi = self.norm.boundaries + v = 0.5*(bi[:-1] + bi[1:]) + self._boundaries = b + self._values = v + return + else: + b = self._uniform_y(self.cmap.N+1) + + self._process_values(b) + + + def _uniform_y(self, N): + ''' + Return colorbar data coordinates for *N* uniformly + spaced boundaries. + ''' + vmin, vmax = self._get_colorbar_limits() + if isinstance(self.norm, colors.LogNorm): + y = np.logspace(np.log10(vmin), np.log10(vmax), N) + else: + y = np.linspace(vmin, vmax, N) + return y + + def _mesh(self): + ''' + Return X,Y, the coordinate arrays for the colorbar pcolormesh. + These are suitable for a vertical colorbar; swapping and + transposition for a horizontal colorbar are done outside + this function. + ''' + x = np.array([1.0, 2.0]) + if self.spacing == 'uniform': + y = self._uniform_y(len(self._boundaries)) + else: + y = self._boundaries + self._y = y + + X, Y = np.meshgrid(x,y) + return X, Y + + + def set_alpha(self, alpha): + """ + set alpha value. + """ + self.alpha = alpha + + +class Colorbar(ColorbarBase): + def __init__(self, ax, mappable, **kw): + mappable.autoscale_None() # Ensure mappable.norm.vmin, vmax + # are set when colorbar is called, + # even if mappable.draw has not yet + # been called. This will not change + # vmin, vmax if they are already set. + self.mappable = mappable + kw['cmap'] = mappable.cmap + kw['norm'] = mappable.norm + kw['alpha'] = mappable.get_alpha() + if isinstance(mappable, contour.ContourSet): + CS = mappable + kw['boundaries'] = CS._levels + kw['values'] = CS.cvalues + kw['extend'] = CS.extend + #kw['ticks'] = CS._levels + kw.setdefault('ticks', ticker.FixedLocator(CS.levels, nbins=10)) + kw['filled'] = CS.filled + ColorbarBase.__init__(self, ax, **kw) + if not CS.filled: + self.add_lines(CS) + else: + ColorbarBase.__init__(self, ax, **kw) + + + def add_lines(self, CS): + ''' + Add the lines from a non-filled + :class:`~matplotlib.contour.ContourSet` to the colorbar. + ''' + if not isinstance(CS, contour.ContourSet) or CS.filled: + raise ValueError('add_lines is only for a ContourSet of lines') + tcolors = [c[0] for c in CS.tcolors] + tlinewidths = [t[0] for t in CS.tlinewidths] + # The following was an attempt to get the colorbar lines + # to follow subsequent changes in the contour lines, + # but more work is needed: specifically, a careful + # look at event sequences, and at how + # to make one object track another automatically. + #tcolors = [col.get_colors()[0] for col in CS.collections] + #tlinewidths = [col.get_linewidth()[0] for lw in CS.collections] + ColorbarBase.add_lines(self, CS.levels, tcolors, tlinewidths) + + def update_bruteforce(self, mappable): + """ + Update the colorbar artists to reflect the change of the + associated mappable. + """ + self.update_artists() + + if isinstance(mappable, contour.ContourSet): + if not mappable.filled: + self.add_lines(mappable) + +@docstring.Substitution(make_axes_kw_doc) +def make_axes(parent, **kw): + ''' + Resize and reposition a parent axes, and return a child + axes suitable for a colorbar + + :: + + cax, kw = make_axes(parent, **kw) + + Keyword arguments may include the following (with defaults): + + *orientation* + 'vertical' or 'horizontal' + + %s + + All but the first of these are stripped from the input kw set. + + Returns (cax, kw), the child axes and the reduced kw dictionary. + ''' + orientation = kw.setdefault('orientation', 'vertical') + fraction = kw.pop('fraction', 0.15) + shrink = kw.pop('shrink', 1.0) + aspect = kw.pop('aspect', 20) + #pb = transforms.PBox(parent.get_position()) + pb = parent.get_position(original=True).frozen() + if orientation == 'vertical': + pad = kw.pop('pad', 0.05) + x1 = 1.0-fraction + pb1, pbx, pbcb = pb.splitx(x1-pad, x1) + pbcb = pbcb.shrunk(1.0, shrink).anchored('C', pbcb) + anchor = (0.0, 0.5) + panchor = (1.0, 0.5) + else: + pad = kw.pop('pad', 0.15) + pbcb, pbx, pb1 = pb.splity(fraction, fraction+pad) + pbcb = pbcb.shrunk(shrink, 1.0).anchored('C', pbcb) + aspect = 1.0/aspect + anchor = (0.5, 1.0) + panchor = (0.5, 0.0) + parent.set_position(pb1) + parent.set_anchor(panchor) + fig = parent.get_figure() + cax = fig.add_axes(pbcb) + cax.set_aspect(aspect, anchor=anchor, adjustable='box') + return cax, kw + +@docstring.Substitution(colorbar_doc) +def colorbar(mappable, cax=None, ax=None, **kw): + """ + Create a colorbar for a ScalarMappable instance. + + Documentation for the pylab thin wrapper: + + %s + """ + import matplotlib.pyplot as plt + if ax is None: + ax = plt.gca() + if cax is None: + cax, kw = make_axes(ax, **kw) + cax._hold = True + cb = Colorbar(cax, mappable, **kw) + + def on_changed(m): + cb.set_cmap(m.get_cmap()) + cb.set_clim(m.get_clim()) + cb.update_bruteforce(m) + + cbid = mappable.callbacksSM.connect('changed', on_changed) + mappable.colorbar = cb + ax.figure.sca(ax) + return cb diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/inset_locator.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/inset_locator.py new file mode 100644 index 0000000000..9aeedcb088 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/inset_locator.py @@ -0,0 +1,659 @@ +""" +A collection of functions and objects for creating or placing inset axes. +""" +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import warnings +from matplotlib import docstring +import six +from matplotlib.offsetbox import AnchoredOffsetbox +from matplotlib.patches import Patch, Rectangle +from matplotlib.path import Path +from matplotlib.transforms import Bbox, BboxTransformTo +from matplotlib.transforms import IdentityTransform, TransformedBbox + +from . import axes_size as Size +from .parasite_axes import HostAxes + + +class InsetPosition(object): + @docstring.dedent_interpd + def __init__(self, parent, lbwh): + """ + An object for positioning an inset axes. + + This is created by specifying the normalized coordinates in the axes, + instead of the figure. + + Parameters + ---------- + parent : `matplotlib.axes.Axes` + Axes to use for normalizing coordinates. + + lbwh : iterable of four floats + The left edge, bottom edge, width, and height of the inset axes, in + units of the normalized coordinate of the *parent* axes. + + See Also + -------- + :meth:`matplotlib.axes.Axes.set_axes_locator` + + Examples + -------- + The following bounds the inset axes to a box with 20%% of the parent + axes's height and 40%% of the width. The size of the axes specified + ([0, 0, 1, 1]) ensures that the axes completely fills the bounding box: + + >>> parent_axes = plt.gca() + >>> ax_ins = plt.axes([0, 0, 1, 1]) + >>> ip = InsetPosition(ax, [0.5, 0.1, 0.4, 0.2]) + >>> ax_ins.set_axes_locator(ip) + """ + self.parent = parent + self.lbwh = lbwh + + def __call__(self, ax, renderer): + bbox_parent = self.parent.get_position(original=False) + trans = BboxTransformTo(bbox_parent) + bbox_inset = Bbox.from_bounds(*self.lbwh) + bb = TransformedBbox(bbox_inset, trans) + return bb + + +class AnchoredLocatorBase(AnchoredOffsetbox): + def __init__(self, bbox_to_anchor, offsetbox, loc, + borderpad=0.5, bbox_transform=None): + super(AnchoredLocatorBase, self).__init__( + loc, pad=0., child=None, borderpad=borderpad, + bbox_to_anchor=bbox_to_anchor, bbox_transform=bbox_transform + ) + + def draw(self, renderer): + raise RuntimeError("No draw method should be called") + + def __call__(self, ax, renderer): + self.axes = ax + + fontsize = renderer.points_to_pixels(self.prop.get_size_in_points()) + self._update_offset_func(renderer, fontsize) + + width, height, xdescent, ydescent = self.get_extent(renderer) + + px, py = self.get_offset(width, height, 0, 0, renderer) + bbox_canvas = Bbox.from_bounds(px, py, width, height) + tr = ax.figure.transFigure.inverted() + bb = TransformedBbox(bbox_canvas, tr) + + return bb + + +class AnchoredSizeLocator(AnchoredLocatorBase): + def __init__(self, bbox_to_anchor, x_size, y_size, loc, + borderpad=0.5, bbox_transform=None): + + super(AnchoredSizeLocator, self).__init__( + bbox_to_anchor, None, loc, + borderpad=borderpad, bbox_transform=bbox_transform + ) + + self.x_size = Size.from_any(x_size) + self.y_size = Size.from_any(y_size) + + def get_extent(self, renderer): + x, y, w, h = self.get_bbox_to_anchor().bounds + + dpi = renderer.points_to_pixels(72.) + + r, a = self.x_size.get_size(renderer) + width = w * r + a * dpi + + r, a = self.y_size.get_size(renderer) + height = h * r + a * dpi + xd, yd = 0, 0 + + fontsize = renderer.points_to_pixels(self.prop.get_size_in_points()) + pad = self.pad * fontsize + + return width + 2 * pad, height + 2 * pad, xd + pad, yd + pad + + +class AnchoredZoomLocator(AnchoredLocatorBase): + def __init__(self, parent_axes, zoom, loc, + borderpad=0.5, + bbox_to_anchor=None, + bbox_transform=None): + self.parent_axes = parent_axes + self.zoom = zoom + + if bbox_to_anchor is None: + bbox_to_anchor = parent_axes.bbox + + super(AnchoredZoomLocator, self).__init__( + bbox_to_anchor, None, loc, borderpad=borderpad, + bbox_transform=bbox_transform) + + def get_extent(self, renderer): + bb = TransformedBbox(self.axes.viewLim, + self.parent_axes.transData) + + x, y, w, h = bb.bounds + fontsize = renderer.points_to_pixels(self.prop.get_size_in_points()) + pad = self.pad * fontsize + + return abs(w * self.zoom) + 2 * pad, abs(h * self.zoom) + 2 * pad, pad, pad + + +class BboxPatch(Patch): + @docstring.dedent_interpd + def __init__(self, bbox, **kwargs): + """ + Patch showing the shape bounded by a Bbox. + + Parameters + ---------- + bbox : `matplotlib.transforms.Bbox` + Bbox to use for the extents of this patch. + + **kwargs + Patch properties. Valid arguments include: + %(Patch)s + """ + if "transform" in kwargs: + raise ValueError("transform should not be set") + + kwargs["transform"] = IdentityTransform() + Patch.__init__(self, **kwargs) + self.bbox = bbox + + def get_path(self): + x0, y0, x1, y1 = self.bbox.extents + + verts = [(x0, y0), + (x1, y0), + (x1, y1), + (x0, y1), + (x0, y0), + (0, 0)] + + codes = [Path.MOVETO, + Path.LINETO, + Path.LINETO, + Path.LINETO, + Path.LINETO, + Path.CLOSEPOLY] + + return Path(verts, codes) + + get_path.__doc__ = Patch.get_path.__doc__ + + +class BboxConnector(Patch): + @staticmethod + def get_bbox_edge_pos(bbox, loc): + """ + Helper function to obtain the location of a corner of a bbox + + Parameters + ---------- + bbox : `matplotlib.transforms.Bbox` + + loc : {1, 2, 3, 4} + Corner of *bbox*. Valid values are:: + + 'upper right' : 1, + 'upper left' : 2, + 'lower left' : 3, + 'lower right' : 4 + + Returns + ------- + x, y : float + Coordinates of the corner specified by *loc*. + """ + x0, y0, x1, y1 = bbox.extents + if loc == 1: + return x1, y1 + elif loc == 2: + return x0, y1 + elif loc == 3: + return x0, y0 + elif loc == 4: + return x1, y0 + + @staticmethod + def connect_bbox(bbox1, bbox2, loc1, loc2=None): + """ + Helper function to obtain a Path from one bbox to another. + + Parameters + ---------- + bbox1, bbox2 : `matplotlib.transforms.Bbox` + Bounding boxes to connect. + + loc1 : {1, 2, 3, 4} + Corner of *bbox1* to use. Valid values are:: + + 'upper right' : 1, + 'upper left' : 2, + 'lower left' : 3, + 'lower right' : 4 + + loc2 : {1, 2, 3, 4}, optional + Corner of *bbox2* to use. If None, defaults to *loc1*. + Valid values are:: + + 'upper right' : 1, + 'upper left' : 2, + 'lower left' : 3, + 'lower right' : 4 + + Returns + ------- + path : `matplotlib.path.Path` + A line segment from the *loc1* corner of *bbox1* to the *loc2* + corner of *bbox2*. + """ + if isinstance(bbox1, Rectangle): + transform = bbox1.get_transfrom() + bbox1 = Bbox.from_bounds(0, 0, 1, 1) + bbox1 = TransformedBbox(bbox1, transform) + + if isinstance(bbox2, Rectangle): + transform = bbox2.get_transform() + bbox2 = Bbox.from_bounds(0, 0, 1, 1) + bbox2 = TransformedBbox(bbox2, transform) + + if loc2 is None: + loc2 = loc1 + + x1, y1 = BboxConnector.get_bbox_edge_pos(bbox1, loc1) + x2, y2 = BboxConnector.get_bbox_edge_pos(bbox2, loc2) + + verts = [[x1, y1], [x2, y2]] + codes = [Path.MOVETO, Path.LINETO] + + return Path(verts, codes) + + @docstring.dedent_interpd + def __init__(self, bbox1, bbox2, loc1, loc2=None, **kwargs): + """ + Connect two bboxes with a straight line. + + Parameters + ---------- + bbox1, bbox2 : `matplotlib.transforms.Bbox` + Bounding boxes to connect. + + loc1 : {1, 2, 3, 4} + Corner of *bbox1* to draw the line. Valid values are:: + + 'upper right' : 1, + 'upper left' : 2, + 'lower left' : 3, + 'lower right' : 4 + + loc2 : {1, 2, 3, 4}, optional + Corner of *bbox2* to draw the line. If None, defaults to *loc1*. + Valid values are:: + + 'upper right' : 1, + 'upper left' : 2, + 'lower left' : 3, + 'lower right' : 4 + + **kwargs + Patch properties for the line drawn. Valid arguments include: + %(Patch)s + """ + if "transform" in kwargs: + raise ValueError("transform should not be set") + + kwargs["transform"] = IdentityTransform() + Patch.__init__(self, fill=False, **kwargs) + self.bbox1 = bbox1 + self.bbox2 = bbox2 + self.loc1 = loc1 + self.loc2 = loc2 + + def get_path(self): + return self.connect_bbox(self.bbox1, self.bbox2, + self.loc1, self.loc2) + + get_path.__doc__ = Patch.get_path.__doc__ + + +class BboxConnectorPatch(BboxConnector): + @docstring.dedent_interpd + def __init__(self, bbox1, bbox2, loc1a, loc2a, loc1b, loc2b, **kwargs): + """ + Connect two bboxes with a quadrilateral. + + The quadrilateral is specified by two lines that start and end at corners + of the bboxes. The four sides of the quadrilateral are defined by the two + lines given, the line between the two corners specified in *bbox1* and the + line between the two corners specified in *bbox2*. + + Parameters + ---------- + bbox1, bbox2 : `matplotlib.transforms.Bbox` + Bounding boxes to connect. + + loc1a, loc2a : {1, 2, 3, 4} + Corners of *bbox1* and *bbox2* to draw the first line. + Valid values are:: + + 'upper right' : 1, + 'upper left' : 2, + 'lower left' : 3, + 'lower right' : 4 + + loc1b, loc2b : {1, 2, 3, 4} + Corners of *bbox1* and *bbox2* to draw the second line. + Valid values are:: + + 'upper right' : 1, + 'upper left' : 2, + 'lower left' : 3, + 'lower right' : 4 + + **kwargs + Patch properties for the line drawn: + %(Patch)s + """ + if "transform" in kwargs: + raise ValueError("transform should not be set") + BboxConnector.__init__(self, bbox1, bbox2, loc1a, loc2a, **kwargs) + self.loc1b = loc1b + self.loc2b = loc2b + + def get_path(self): + path1 = self.connect_bbox(self.bbox1, self.bbox2, self.loc1, self.loc2) + path2 = self.connect_bbox(self.bbox2, self.bbox1, + self.loc2b, self.loc1b) + path_merged = (list(path1.vertices) + + list(path2.vertices) + + [path1.vertices[0]]) + return Path(path_merged) + + get_path.__doc__ = BboxConnector.get_path.__doc__ + + +def _add_inset_axes(parent_axes, inset_axes): + """Helper function to add an inset axes and disable navigation in it""" + parent_axes.figure.add_axes(inset_axes) + inset_axes.set_navigate(False) + + +@docstring.dedent_interpd +def inset_axes(parent_axes, width, height, loc=1, + bbox_to_anchor=None, bbox_transform=None, + axes_class=None, + axes_kwargs=None, + borderpad=0.5): + """ + Create an inset axes with a given width and height. + + Both sizes used can be specified either in inches or percentage. + For example,:: + + inset_axes(parent_axes, width='40%%', height='30%%', loc=3) + + creates in inset axes in the lower left corner of *parent_axes* which spans + over 30%% in height and 40%% in width of the *parent_axes*. Since the usage + of `.inset_axes` may become slightly tricky when exceeding such standard + cases, it is recommended to read + :ref:`the examples <sphx_glr_gallery_axes_grid1_inset_locator_demo.py>`. + + Parameters + ---------- + parent_axes : `matplotlib.axes.Axes` + Axes to place the inset axes. + + width, height : float or str + Size of the inset axes to create. If a float is provided, it is + the size in inches, e.g. *width=1.3*. If a string is provided, it is + the size in relative units, e.g. *width='40%%'*. By default, i.e. if + neither *bbox_to_anchor* nor *bbox_transform* are specified, those + are relative to the parent_axes. Otherwise they are to be understood + relative to the bounding box provided via *bbox_to_anchor*. + + loc : int or string, optional, default to 1 + Location to place the inset axes. The valid locations are:: + + 'upper right' : 1, + 'upper left' : 2, + 'lower left' : 3, + 'lower right' : 4, + 'right' : 5, + 'center left' : 6, + 'center right' : 7, + 'lower center' : 8, + 'upper center' : 9, + 'center' : 10 + + bbox_to_anchor : tuple or `matplotlib.transforms.BboxBase`, optional + Bbox that the inset axes will be anchored to. If None, + *parent_axes.bbox* is used. If a tuple, can be either + [left, bottom, width, height], or [left, bottom]. + If the kwargs *width* and/or *height* are specified in relative units, + the 2-tuple [left, bottom] cannot be used. Note that + the units of the bounding box are determined through the transform + in use. When using *bbox_to_anchor* it almost always makes sense to + also specify a *bbox_transform*. This might often be the axes transform + *parent_axes.transAxes*. + + bbox_transform : `matplotlib.transforms.Transform`, optional + Transformation for the bbox that contains the inset axes. + If None, a `.transforms.IdentityTransform` is used (i.e. pixel + coordinates). This is useful when not providing any argument to + *bbox_to_anchor*. When using *bbox_to_anchor* it almost always makes + sense to also specify a *bbox_transform*. This might often be the + axes transform *parent_axes.transAxes*. Inversely, when specifying + the axes- or figure-transform here, be aware that not specifying + *bbox_to_anchor* will use *parent_axes.bbox*, the units of which are + in display (pixel) coordinates. + + axes_class : `matplotlib.axes.Axes` type, optional + If specified, the inset axes created will be created with this class's + constructor. + + axes_kwargs : dict, optional + Keyworded arguments to pass to the constructor of the inset axes. + Valid arguments include: + %(Axes)s + + borderpad : float, optional + Padding between inset axes and the bbox_to_anchor. Defaults to 0.5. + The units are axes font size, i.e. for a default font size of 10 points + *borderpad = 0.5* is equivalent to a padding of 5 points. + + Returns + ------- + inset_axes : `axes_class` + Inset axes object created. + """ + + if axes_class is None: + axes_class = HostAxes + + if axes_kwargs is None: + inset_axes = axes_class(parent_axes.figure, parent_axes.get_position()) + else: + inset_axes = axes_class(parent_axes.figure, parent_axes.get_position(), + **axes_kwargs) + + if bbox_transform in [parent_axes.transAxes, + parent_axes.figure.transFigure]: + if bbox_to_anchor is None: + warnings.warn("Using the axes or figure transform requires a " + "bounding box in the respective coordinates. " + "Using bbox_to_anchor=(0,0,1,1) now.") + bbox_to_anchor = (0, 0, 1, 1) + + if bbox_to_anchor is None: + bbox_to_anchor = parent_axes.bbox + + if isinstance(bbox_to_anchor, tuple) and \ + (isinstance(width, str) or isinstance(height, str)): + if len(bbox_to_anchor) != 4: + raise ValueError("Using relative units for width or height " + "requires to provide a 4-tuple or a " + "`BBox` instance to `bbox_to_anchor.") + + axes_locator = AnchoredSizeLocator(bbox_to_anchor, + width, height, + loc=loc, + bbox_transform=bbox_transform, + borderpad=borderpad) + + inset_axes.set_axes_locator(axes_locator) + + _add_inset_axes(parent_axes, inset_axes) + + return inset_axes + + +@docstring.dedent_interpd +def zoomed_inset_axes(parent_axes, zoom, loc=1, + bbox_to_anchor=None, bbox_transform=None, + axes_class=None, + axes_kwargs=None, + borderpad=0.5): + """ + Create an anchored inset axes by scaling a parent axes. For usage, also see + :ref:`the examples <sphx_glr_gallery_axes_grid1_inset_locator_demo2.py>`. + + Parameters + ---------- + parent_axes : `matplotlib.axes.Axes` + Axes to place the inset axes. + + zoom : float + Scaling factor of the data axes. *zoom* > 1 will enlargen the + coordinates (i.e., "zoomed in"), while *zoom* < 1 will shrink the + coordinates (i.e., "zoomed out"). + + loc : int or string, optional, default to 1 + Location to place the inset axes. The valid locations are:: + + 'upper right' : 1, + 'upper left' : 2, + 'lower left' : 3, + 'lower right' : 4, + 'right' : 5, + 'center left' : 6, + 'center right' : 7, + 'lower center' : 8, + 'upper center' : 9, + 'center' : 10 + + bbox_to_anchor : tuple or `matplotlib.transforms.BboxBase`, optional + Bbox that the inset axes will be anchored to. If None, + *parent_axes.bbox* is used. If a tuple, can be either + [left, bottom, width, height], or [left, bottom]. + If the kwargs *width* and/or *height* are specified in relative units, + the 2-tuple [left, bottom] cannot be used. Note that + the units of the bounding box are determined through the transform + in use. When using *bbox_to_anchor* it almost always makes sense to + also specify a *bbox_transform*. This might often be the axes transform + *parent_axes.transAxes*. + + bbox_transform : `matplotlib.transforms.Transform`, optional + Transformation for the bbox that contains the inset axes. + If None, a `.transforms.IdentityTransform` is used (i.e. pixel + coordinates). This is useful when not providing any argument to + *bbox_to_anchor*. When using *bbox_to_anchor* it almost always makes + sense to also specify a *bbox_transform*. This might often be the + axes transform *parent_axes.transAxes*. Inversely, when specifying + the axes- or figure-transform here, be aware that not specifying + *bbox_to_anchor* will use *parent_axes.bbox*, the units of which are + in display (pixel) coordinates. + + axes_class : `matplotlib.axes.Axes` type, optional + If specified, the inset axes created will be created with this class's + constructor. + + axes_kwargs : dict, optional + Keyworded arguments to pass to the constructor of the inset axes. + Valid arguments include: + %(Axes)s + + borderpad : float, optional + Padding between inset axes and the bbox_to_anchor. Defaults to 0.5. + The units are axes font size, i.e. for a default font size of 10 points + *borderpad = 0.5* is equivalent to a padding of 5 points. + + Returns + ------- + inset_axes : `axes_class` + Inset axes object created. + """ + + if axes_class is None: + axes_class = HostAxes + + if axes_kwargs is None: + inset_axes = axes_class(parent_axes.figure, parent_axes.get_position()) + else: + inset_axes = axes_class(parent_axes.figure, parent_axes.get_position(), + **axes_kwargs) + + axes_locator = AnchoredZoomLocator(parent_axes, zoom=zoom, loc=loc, + bbox_to_anchor=bbox_to_anchor, + bbox_transform=bbox_transform, + borderpad=borderpad) + inset_axes.set_axes_locator(axes_locator) + + _add_inset_axes(parent_axes, inset_axes) + + return inset_axes + + +@docstring.dedent_interpd +def mark_inset(parent_axes, inset_axes, loc1, loc2, **kwargs): + """ + Draw a box to mark the location of an area represented by an inset axes. + + This function draws a box in *parent_axes* at the bounding box of + *inset_axes*, and shows a connection with the inset axes by drawing lines + at the corners, giving a "zoomed in" effect. + + Parameters + ---------- + parent_axes : `matplotlib.axes.Axes` + Axes which contains the area of the inset axes. + + inset_axes : `matplotlib.axes.Axes` + The inset axes. + + loc1, loc2 : {1, 2, 3, 4} + Corners to use for connecting the inset axes and the area in the + parent axes. + + **kwargs + Patch properties for the lines and box drawn: + %(Patch)s + + Returns + ------- + pp : `matplotlib.patches.Patch` + The patch drawn to represent the area of the inset axes. + + p1, p2 : `matplotlib.patches.Patch` + The patches connecting two corners of the inset axes and its area. + """ + rect = TransformedBbox(inset_axes.viewLim, parent_axes.transData) + + fill = kwargs.pop("fill", False) + pp = BboxPatch(rect, fill=fill, **kwargs) + parent_axes.add_patch(pp) + + p1 = BboxConnector(inset_axes.bbox, rect, loc1=loc1, **kwargs) + inset_axes.add_patch(p1) + p1.set_clip_on(False) + p2 = BboxConnector(inset_axes.bbox, rect, loc1=loc2, **kwargs) + inset_axes.add_patch(p2) + p2.set_clip_on(False) + + return pp, p1, p2 diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/mpl_axes.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/mpl_axes.py new file mode 100644 index 0000000000..aaff7b7692 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/mpl_axes.py @@ -0,0 +1,154 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six + +import matplotlib.axes as maxes +from matplotlib.artist import Artist +from matplotlib.axis import XAxis, YAxis + +class SimpleChainedObjects(object): + def __init__(self, objects): + self._objects = objects + + def __getattr__(self, k): + _a = SimpleChainedObjects([getattr(a, k) for a in self._objects]) + return _a + + def __call__(self, *kl, **kwargs): + for m in self._objects: + m(*kl, **kwargs) + + +class Axes(maxes.Axes): + + class AxisDict(dict): + def __init__(self, axes): + self.axes = axes + super(Axes.AxisDict, self).__init__() + + def __getitem__(self, k): + if isinstance(k, tuple): + r = SimpleChainedObjects( + [super(Axes.AxisDict, self).__getitem__(k1) for k1 in k]) + return r + elif isinstance(k, slice): + if k.start is None and k.stop is None and k.step is None: + r = SimpleChainedObjects(list(six.itervalues(self))) + return r + else: + raise ValueError("Unsupported slice") + else: + return dict.__getitem__(self, k) + + def __call__(self, *v, **kwargs): + return maxes.Axes.axis(self.axes, *v, **kwargs) + + def __init__(self, *kl, **kw): + super(Axes, self).__init__(*kl, **kw) + + def _init_axis_artists(self, axes=None): + if axes is None: + axes = self + + self._axislines = self.AxisDict(self) + + self._axislines["bottom"] = SimpleAxisArtist(self.xaxis, 1, self.spines["bottom"]) + self._axislines["top"] = SimpleAxisArtist(self.xaxis, 2, self.spines["top"]) + self._axislines["left"] = SimpleAxisArtist(self.yaxis, 1, self.spines["left"]) + self._axislines["right"] = SimpleAxisArtist(self.yaxis, 2, self.spines["right"]) + + + def _get_axislines(self): + return self._axislines + + axis = property(_get_axislines) + + def cla(self): + + super(Axes, self).cla() + self._init_axis_artists() + + +class SimpleAxisArtist(Artist): + def __init__(self, axis, axisnum, spine): + self._axis = axis + self._axisnum = axisnum + self.line = spine + + if isinstance(axis, XAxis): + self._axis_direction = ["bottom", "top"][axisnum-1] + elif isinstance(axis, YAxis): + self._axis_direction = ["left", "right"][axisnum-1] + else: + raise ValueError("axis must be instance of XAxis or YAxis : %s is provided" % (axis,)) + Artist.__init__(self) + + + def _get_major_ticks(self): + tickline = "tick%dline" % self._axisnum + return SimpleChainedObjects([getattr(tick, tickline) + for tick in self._axis.get_major_ticks()]) + + def _get_major_ticklabels(self): + label = "label%d" % self._axisnum + return SimpleChainedObjects([getattr(tick, label) + for tick in self._axis.get_major_ticks()]) + + def _get_label(self): + return self._axis.label + + major_ticks = property(_get_major_ticks) + major_ticklabels = property(_get_major_ticklabels) + label = property(_get_label) + + def set_visible(self, b): + self.toggle(all=b) + self.line.set_visible(b) + self._axis.set_visible(True) + Artist.set_visible(self, b) + + def set_label(self, txt): + self._axis.set_label_text(txt) + + def toggle(self, all=None, ticks=None, ticklabels=None, label=None): + + if all: + _ticks, _ticklabels, _label = True, True, True + elif all is not None: + _ticks, _ticklabels, _label = False, False, False + else: + _ticks, _ticklabels, _label = None, None, None + + if ticks is not None: + _ticks = ticks + if ticklabels is not None: + _ticklabels = ticklabels + if label is not None: + _label = label + + tickOn = "tick%dOn" % self._axisnum + labelOn = "label%dOn" % self._axisnum + + if _ticks is not None: + tickparam = {tickOn: _ticks} + self._axis.set_tick_params(**tickparam) + if _ticklabels is not None: + tickparam = {labelOn: _ticklabels} + self._axis.set_tick_params(**tickparam) + + if _label is not None: + pos = self._axis.get_label_position() + if (pos == self._axis_direction) and not _label: + self._axis.label.set_visible(False) + elif _label: + self._axis.label.set_visible(True) + self._axis.set_label_position(self._axis_direction) + + +if __name__ == '__main__': + import matplotlib.pyplot as plt + fig = plt.figure() + ax = Axes(fig, [0.1, 0.1, 0.8, 0.8]) + fig.add_axes(ax) + ax.cla() diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/parasite_axes.py b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/parasite_axes.py new file mode 100644 index 0000000000..16a67b4d1f --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axes_grid1/parasite_axes.py @@ -0,0 +1,486 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six + +from matplotlib import ( + artist as martist, collections as mcoll, transforms as mtransforms, + rcParams) +from matplotlib.axes import subplot_class_factory +from matplotlib.transforms import Bbox +from .mpl_axes import Axes + +import numpy as np + + +class ParasiteAxesBase(object): + + def get_images_artists(self): + artists = {a for a in self.get_children() if a.get_visible()} + images = {a for a in self.images if a.get_visible()} + + return list(images), list(artists - images) + + def __init__(self, parent_axes, **kargs): + + self._parent_axes = parent_axes + kargs.update(dict(frameon=False)) + self._get_base_axes_attr("__init__")(self, parent_axes.figure, + parent_axes._position, **kargs) + + def cla(self): + self._get_base_axes_attr("cla")(self) + + martist.setp(self.get_children(), visible=False) + self._get_lines = self._parent_axes._get_lines + + # In mpl's Axes, zorders of x- and y-axis are originally set + # within Axes.draw(). + if self._axisbelow: + self.xaxis.set_zorder(0.5) + self.yaxis.set_zorder(0.5) + else: + self.xaxis.set_zorder(2.5) + self.yaxis.set_zorder(2.5) + + +_parasite_axes_classes = {} +def parasite_axes_class_factory(axes_class=None): + if axes_class is None: + axes_class = Axes + + new_class = _parasite_axes_classes.get(axes_class) + if new_class is None: + def _get_base_axes_attr(self, attrname): + return getattr(axes_class, attrname) + + new_class = type(str("%sParasite" % (axes_class.__name__)), + (ParasiteAxesBase, axes_class), + {'_get_base_axes_attr': _get_base_axes_attr}) + _parasite_axes_classes[axes_class] = new_class + + return new_class + +ParasiteAxes = parasite_axes_class_factory() + +# #class ParasiteAxes(ParasiteAxesBase, Axes): + +# @classmethod +# def _get_base_axes_attr(cls, attrname): +# return getattr(Axes, attrname) + + + +class ParasiteAxesAuxTransBase(object): + def __init__(self, parent_axes, aux_transform, viewlim_mode=None, + **kwargs): + + self.transAux = aux_transform + self.set_viewlim_mode(viewlim_mode) + + self._parasite_axes_class.__init__(self, parent_axes, **kwargs) + + def _set_lim_and_transforms(self): + + self.transAxes = self._parent_axes.transAxes + + self.transData = \ + self.transAux + \ + self._parent_axes.transData + + self._xaxis_transform = mtransforms.blended_transform_factory( + self.transData, self.transAxes) + self._yaxis_transform = mtransforms.blended_transform_factory( + self.transAxes, self.transData) + + def set_viewlim_mode(self, mode): + if mode not in [None, "equal", "transform"]: + raise ValueError("Unknown mode : %s" % (mode,)) + else: + self._viewlim_mode = mode + + def get_viewlim_mode(self): + return self._viewlim_mode + + + def update_viewlim(self): + viewlim = self._parent_axes.viewLim.frozen() + mode = self.get_viewlim_mode() + if mode is None: + pass + elif mode == "equal": + self.axes.viewLim.set(viewlim) + elif mode == "transform": + self.axes.viewLim.set(viewlim.transformed(self.transAux.inverted())) + else: + raise ValueError("Unknown mode : %s" % (self._viewlim_mode,)) + + + def _pcolor(self, method_name, *XYC, **kwargs): + if len(XYC) == 1: + C = XYC[0] + ny, nx = C.shape + + gx = np.arange(-0.5, nx, 1.) + gy = np.arange(-0.5, ny, 1.) + + X, Y = np.meshgrid(gx, gy) + else: + X, Y, C = XYC + + pcolor_routine = self._get_base_axes_attr(method_name) + + if "transform" in kwargs: + mesh = pcolor_routine(self, X, Y, C, **kwargs) + else: + orig_shape = X.shape + xy = np.vstack([X.flat, Y.flat]) + xyt=xy.transpose() + wxy = self.transAux.transform(xyt) + gx, gy = wxy[:,0].reshape(orig_shape), wxy[:,1].reshape(orig_shape) + mesh = pcolor_routine(self, gx, gy, C, **kwargs) + mesh.set_transform(self._parent_axes.transData) + + return mesh + + def pcolormesh(self, *XYC, **kwargs): + return self._pcolor("pcolormesh", *XYC, **kwargs) + + def pcolor(self, *XYC, **kwargs): + return self._pcolor("pcolor", *XYC, **kwargs) + + + def _contour(self, method_name, *XYCL, **kwargs): + + if len(XYCL) <= 2: + C = XYCL[0] + ny, nx = C.shape + + gx = np.arange(0., nx, 1.) + gy = np.arange(0., ny, 1.) + + X,Y = np.meshgrid(gx, gy) + CL = XYCL + else: + X, Y = XYCL[:2] + CL = XYCL[2:] + + contour_routine = self._get_base_axes_attr(method_name) + + if "transform" in kwargs: + cont = contour_routine(self, X, Y, *CL, **kwargs) + else: + orig_shape = X.shape + xy = np.vstack([X.flat, Y.flat]) + xyt=xy.transpose() + wxy = self.transAux.transform(xyt) + gx, gy = wxy[:,0].reshape(orig_shape), wxy[:,1].reshape(orig_shape) + cont = contour_routine(self, gx, gy, *CL, **kwargs) + for c in cont.collections: + c.set_transform(self._parent_axes.transData) + + return cont + + def contour(self, *XYCL, **kwargs): + return self._contour("contour", *XYCL, **kwargs) + + def contourf(self, *XYCL, **kwargs): + return self._contour("contourf", *XYCL, **kwargs) + + def apply_aspect(self, position=None): + self.update_viewlim() + self._get_base_axes_attr("apply_aspect")(self) + #ParasiteAxes.apply_aspect() + + + +_parasite_axes_auxtrans_classes = {} +def parasite_axes_auxtrans_class_factory(axes_class=None): + if axes_class is None: + parasite_axes_class = ParasiteAxes + elif not issubclass(axes_class, ParasiteAxesBase): + parasite_axes_class = parasite_axes_class_factory(axes_class) + else: + parasite_axes_class = axes_class + + new_class = _parasite_axes_auxtrans_classes.get(parasite_axes_class) + if new_class is None: + new_class = type(str("%sParasiteAuxTrans" % (parasite_axes_class.__name__)), + (ParasiteAxesAuxTransBase, parasite_axes_class), + {'_parasite_axes_class': parasite_axes_class, + 'name': 'parasite_axes'}) + _parasite_axes_auxtrans_classes[parasite_axes_class] = new_class + + return new_class + + +ParasiteAxesAuxTrans = parasite_axes_auxtrans_class_factory(axes_class=ParasiteAxes) + + + + +def _get_handles(ax): + handles = ax.lines[:] + handles.extend(ax.patches) + handles.extend([c for c in ax.collections + if isinstance(c, mcoll.LineCollection)]) + handles.extend([c for c in ax.collections + if isinstance(c, mcoll.RegularPolyCollection)]) + handles.extend([c for c in ax.collections + if isinstance(c, mcoll.CircleCollection)]) + + return handles + + +class HostAxesBase(object): + def __init__(self, *args, **kwargs): + + self.parasites = [] + self._get_base_axes_attr("__init__")(self, *args, **kwargs) + + + def get_aux_axes(self, tr, viewlim_mode="equal", axes_class=None): + parasite_axes_class = parasite_axes_auxtrans_class_factory(axes_class) + ax2 = parasite_axes_class(self, tr, viewlim_mode) + # note that ax2.transData == tr + ax1.transData + # Anthing you draw in ax2 will match the ticks and grids of ax1. + self.parasites.append(ax2) + ax2._remove_method = lambda h: self.parasites.remove(h) + return ax2 + + def _get_legend_handles(self, legend_handler_map=None): + # don't use this! + Axes_get_legend_handles = self._get_base_axes_attr("_get_legend_handles") + all_handles = list(Axes_get_legend_handles(self, legend_handler_map)) + + for ax in self.parasites: + all_handles.extend(ax._get_legend_handles(legend_handler_map)) + + return all_handles + + + def draw(self, renderer): + + orig_artists = list(self.artists) + orig_images = list(self.images) + + if hasattr(self, "get_axes_locator"): + locator = self.get_axes_locator() + if locator: + pos = locator(self, renderer) + self.set_position(pos, which="active") + self.apply_aspect(pos) + else: + self.apply_aspect() + else: + self.apply_aspect() + + rect = self.get_position() + + for ax in self.parasites: + ax.apply_aspect(rect) + images, artists = ax.get_images_artists() + self.images.extend(images) + self.artists.extend(artists) + + self._get_base_axes_attr("draw")(self, renderer) + self.artists = orig_artists + self.images = orig_images + + + def cla(self): + + for ax in self.parasites: + ax.cla() + + self._get_base_axes_attr("cla")(self) + #super(HostAxes, self).cla() + + + def twinx(self, axes_class=None): + """ + create a twin of Axes for generating a plot with a sharex + x-axis but independent y axis. The y-axis of self will have + ticks on left and the returned axes will have ticks on the + right + """ + + if axes_class is None: + axes_class = self._get_base_axes() + + parasite_axes_class = parasite_axes_class_factory(axes_class) + + ax2 = parasite_axes_class(self, sharex=self, frameon=False) + self.parasites.append(ax2) + + self.axis["right"].set_visible(False) + + ax2.axis["right"].set_visible(True) + ax2.axis["left", "top", "bottom"].set_visible(False) + + def _remove_method(h): + self.parasites.remove(h) + self.axis["right"].set_visible(True) + self.axis["right"].toggle(ticklabels=False, label=False) + ax2._remove_method = _remove_method + + return ax2 + + def twiny(self, axes_class=None): + """ + create a twin of Axes for generating a plot with a shared + y-axis but independent x axis. The x-axis of self will have + ticks on bottom and the returned axes will have ticks on the + top + """ + + if axes_class is None: + axes_class = self._get_base_axes() + + parasite_axes_class = parasite_axes_class_factory(axes_class) + + ax2 = parasite_axes_class(self, sharey=self, frameon=False) + self.parasites.append(ax2) + + self.axis["top"].set_visible(False) + + ax2.axis["top"].set_visible(True) + ax2.axis["left", "right", "bottom"].set_visible(False) + + def _remove_method(h): + self.parasites.remove(h) + self.axis["top"].set_visible(True) + self.axis["top"].toggle(ticklabels=False, label=False) + ax2._remove_method = _remove_method + + return ax2 + + + def twin(self, aux_trans=None, axes_class=None): + """ + create a twin of Axes for generating a plot with a sharex + x-axis but independent y axis. The y-axis of self will have + ticks on left and the returned axes will have ticks on the + right + """ + + if axes_class is None: + axes_class = self._get_base_axes() + + parasite_axes_auxtrans_class = parasite_axes_auxtrans_class_factory(axes_class) + + if aux_trans is None: + ax2 = parasite_axes_auxtrans_class(self, mtransforms.IdentityTransform(), + viewlim_mode="equal", + ) + else: + ax2 = parasite_axes_auxtrans_class(self, aux_trans, + viewlim_mode="transform", + ) + self.parasites.append(ax2) + ax2._remove_method = lambda h: self.parasites.remove(h) + + self.axis["top", "right"].set_visible(False) + + ax2.axis["top", "right"].set_visible(True) + ax2.axis["left", "bottom"].set_visible(False) + + def _remove_method(h): + self.parasites.remove(h) + self.axis["top", "right"].set_visible(True) + self.axis["top", "right"].toggle(ticklabels=False, label=False) + ax2._remove_method = _remove_method + + return ax2 + + def get_tightbbox(self, renderer, call_axes_locator=True): + + bbs = [ax.get_tightbbox(renderer, call_axes_locator) + for ax in self.parasites] + get_tightbbox = self._get_base_axes_attr("get_tightbbox") + bbs.append(get_tightbbox(self, renderer, call_axes_locator)) + + _bbox = Bbox.union([b for b in bbs if b.width!=0 or b.height!=0]) + + return _bbox + + + +_host_axes_classes = {} +def host_axes_class_factory(axes_class=None): + if axes_class is None: + axes_class = Axes + + new_class = _host_axes_classes.get(axes_class) + if new_class is None: + def _get_base_axes(self): + return axes_class + + def _get_base_axes_attr(self, attrname): + return getattr(axes_class, attrname) + + new_class = type(str("%sHostAxes" % (axes_class.__name__)), + (HostAxesBase, axes_class), + {'_get_base_axes_attr': _get_base_axes_attr, + '_get_base_axes': _get_base_axes}) + + _host_axes_classes[axes_class] = new_class + + return new_class + +def host_subplot_class_factory(axes_class): + host_axes_class = host_axes_class_factory(axes_class=axes_class) + subplot_host_class = subplot_class_factory(host_axes_class) + return subplot_host_class + +HostAxes = host_axes_class_factory(axes_class=Axes) +SubplotHost = subplot_class_factory(HostAxes) + + +def host_axes(*args, **kwargs): + """ + Create axes that can act as a hosts to parasitic axes. + + Parameters + ---------- + figure : `matplotlib.figure.Figure` + Figure to which the axes will be added. Defaults to the current figure + `pyplot.gcf()`. + + *args, **kwargs : + Will be passed on to the underlying ``Axes`` object creation. + """ + import matplotlib.pyplot as plt + axes_class = kwargs.pop("axes_class", None) + host_axes_class = host_axes_class_factory(axes_class) + fig = kwargs.get("figure", None) + if fig is None: + fig = plt.gcf() + ax = host_axes_class(fig, *args, **kwargs) + fig.add_axes(ax) + plt.draw_if_interactive() + return ax + +def host_subplot(*args, **kwargs): + """ + Create a subplot that can act as a host to parasitic axes. + + Parameters + ---------- + figure : `matplotlib.figure.Figure` + Figure to which the subplot will be added. Defaults to the current + figure `pyplot.gcf()`. + + *args, **kwargs : + Will be passed on to the underlying ``Axes`` object creation. + """ + import matplotlib.pyplot as plt + axes_class = kwargs.pop("axes_class", None) + host_subplot_class = host_subplot_class_factory(axes_class) + fig = kwargs.get("figure", None) + if fig is None: + fig = plt.gcf() + ax = host_subplot_class(fig, *args, **kwargs) + fig.add_subplot(ax) + plt.draw_if_interactive() + return ax diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/__init__.py b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/__init__.py new file mode 100644 index 0000000000..8431c0cd3e --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/__init__.py @@ -0,0 +1,26 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six + +from .axislines import ( + Axes, AxesZero, AxisArtistHelper, AxisArtistHelperRectlinear, + GridHelperBase, GridHelperRectlinear, Subplot, SubplotZero) +from .axis_artist import AxisArtist, GridlinesCollection + +from .grid_helper_curvelinear import GridHelperCurveLinear + +from .floating_axes import FloatingAxes, FloatingSubplot + +from mpl_toolkits.axes_grid1.parasite_axes import ( + host_axes_class_factory, parasite_axes_class_factory, + parasite_axes_auxtrans_class_factory, subplot_class_factory) + +ParasiteAxes = parasite_axes_class_factory(Axes) + +ParasiteAxesAuxTrans = \ + parasite_axes_auxtrans_class_factory(axes_class=ParasiteAxes) + +HostAxes = host_axes_class_factory(axes_class=Axes) + +SubplotHost = subplot_class_factory(HostAxes) diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/angle_helper.py b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/angle_helper.py new file mode 100644 index 0000000000..15732a58ec --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/angle_helper.py @@ -0,0 +1,416 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six + +import numpy as np +import math + +from mpl_toolkits.axisartist.grid_finder import ExtremeFinderSimple + +def select_step_degree(dv): + + degree_limits_ = [1.5, 3, 7, 13, 20, 40, 70, 120, 270, 520] + degree_steps_ = [ 1, 2, 5, 10, 15, 30, 45, 90, 180, 360] + degree_factors = [1.] * len(degree_steps_) + + minsec_limits_ = [1.5, 2.5, 3.5, 8, 11, 18, 25, 45] + minsec_steps_ = [1, 2, 3, 5, 10, 15, 20, 30] + + minute_limits_ = np.array(minsec_limits_) / 60 + minute_factors = [60.] * len(minute_limits_) + + second_limits_ = np.array(minsec_limits_) / 3600 + second_factors = [3600.] * len(second_limits_) + + degree_limits = np.concatenate([second_limits_, + minute_limits_, + degree_limits_]) + + degree_steps = np.concatenate([minsec_steps_, + minsec_steps_, + degree_steps_]) + + degree_factors = np.concatenate([second_factors, + minute_factors, + degree_factors]) + + n = degree_limits.searchsorted(dv) + step = degree_steps[n] + factor = degree_factors[n] + + return step, factor + + + +def select_step_hour(dv): + + hour_limits_ = [1.5, 2.5, 3.5, 5, 7, 10, 15, 21, 36] + hour_steps_ = [1, 2 , 3, 4, 6, 8, 12, 18, 24] + hour_factors = [1.] * len(hour_steps_) + + minsec_limits_ = [1.5, 2.5, 3.5, 4.5, 5.5, 8, 11, 14, 18, 25, 45] + minsec_steps_ = [1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30] + + minute_limits_ = np.array(minsec_limits_) / 60 + minute_factors = [60.] * len(minute_limits_) + + second_limits_ = np.array(minsec_limits_) / 3600 + second_factors = [3600.] * len(second_limits_) + + hour_limits = np.concatenate([second_limits_, + minute_limits_, + hour_limits_]) + + hour_steps = np.concatenate([minsec_steps_, + minsec_steps_, + hour_steps_]) + + hour_factors = np.concatenate([second_factors, + minute_factors, + hour_factors]) + + n = hour_limits.searchsorted(dv) + step = hour_steps[n] + factor = hour_factors[n] + + return step, factor + + +def select_step_sub(dv): + + # subarcsec or degree + tmp = 10.**(int(math.log10(dv))-1.) + + factor = 1./tmp + + if 1.5*tmp >= dv: + step = 1 + elif 3.*tmp >= dv: + step = 2 + elif 7.*tmp >= dv: + step = 5 + else: + step = 1 + factor = 0.1*factor + + return step, factor + + +def select_step(v1, v2, nv, hour=False, include_last=True, + threshold_factor=3600.): + + if v1 > v2: + v1, v2 = v2, v1 + + dv = (v2 - v1) / nv + + if hour: + _select_step = select_step_hour + cycle = 24. + else: + _select_step = select_step_degree + cycle = 360. + + # for degree + if dv > 1./threshold_factor: + step, factor = _select_step(dv) + else: + step, factor = select_step_sub(dv*threshold_factor) + + factor = factor * threshold_factor + + + f1, f2, fstep = v1*factor, v2*factor, step/factor + levs = np.arange(np.floor(f1/step), np.ceil(f2/step)+0.5, dtype=int) * step + + # n : number of valid levels. If there is a cycle, e.g., [0, 90, 180, + # 270, 360], the grid line needs to be extended from 0 to 360, so + # we need to return the whole array. However, the last level (360) + # needs to be ignored often. In this case, so we return n=4. + + n = len(levs) + + + # we need to check the range of values + # for example, -90 to 90, 0 to 360, + + if factor == 1. and (levs[-1] >= levs[0]+cycle): # check for cycle + nv = int(cycle / step) + if include_last: + levs = levs[0] + np.arange(0, nv+1, 1) * step + else: + levs = levs[0] + np.arange(0, nv, 1) * step + + n = len(levs) + + return np.array(levs), n, factor + + +def select_step24(v1, v2, nv, include_last=True, threshold_factor=3600): + v1, v2 = v1/15., v2/15. + levs, n, factor = select_step(v1, v2, nv, hour=True, + include_last=include_last, + threshold_factor=threshold_factor) + return levs*15., n, factor + +def select_step360(v1, v2, nv, include_last=True, threshold_factor=3600): + return select_step(v1, v2, nv, hour=False, + include_last=include_last, + threshold_factor=threshold_factor) + + +class LocatorBase(object): + def __init__(self, den, include_last=True): + self.den = den + self._include_last = include_last + + @property + def nbins(self): + return self.den + + @nbins.setter + def nbins(self, v): + self.den = v + + def set_params(self, nbins=None): + if nbins is not None: + self.den = int(nbins) + + +class LocatorHMS(LocatorBase): + def __call__(self, v1, v2): + return select_step24(v1, v2, self.den, self._include_last) + +class LocatorHM(LocatorBase): + def __call__(self, v1, v2): + return select_step24(v1, v2, self.den, self._include_last, + threshold_factor=60) + +class LocatorH(LocatorBase): + def __call__(self, v1, v2): + return select_step24(v1, v2, self.den, self._include_last, + threshold_factor=1) + + +class LocatorDMS(LocatorBase): + def __call__(self, v1, v2): + return select_step360(v1, v2, self.den, self._include_last) + +class LocatorDM(LocatorBase): + def __call__(self, v1, v2): + return select_step360(v1, v2, self.den, self._include_last, + threshold_factor=60) + +class LocatorD(LocatorBase): + def __call__(self, v1, v2): + return select_step360(v1, v2, self.den, self._include_last, + threshold_factor=1) + + +class FormatterDMS(object): + deg_mark = r"^{\circ}" + min_mark = r"^{\prime}" + sec_mark = r"^{\prime\prime}" + + fmt_d = "$%d" + deg_mark + "$" + fmt_ds = r"$%d.%s" + deg_mark + "$" + + # %s for sign + fmt_d_m = r"$%s%d" + deg_mark + r"\,%02d" + min_mark + "$" + fmt_d_ms = r"$%s%d" + deg_mark + r"\,%02d.%s" + min_mark + "$" + + fmt_d_m_partial = "$%s%d" + deg_mark + r"\,%02d" + min_mark + r"\," + fmt_s_partial = "%02d" + sec_mark + "$" + fmt_ss_partial = "%02d.%s" + sec_mark + "$" + + def _get_number_fraction(self, factor): + ## check for fractional numbers + number_fraction = None + # check for 60 + + for threshold in [1, 60, 3600]: + if factor <= threshold: + break + + d = factor // threshold + int_log_d = int(np.floor(np.log10(d))) + if 10**int_log_d == d and d != 1: + number_fraction = int_log_d + factor = factor // 10**int_log_d + return factor, number_fraction + + return factor, number_fraction + + + def __call__(self, direction, factor, values): + if len(values) == 0: + return [] + #ss = [[-1, 1][v>0] for v in values] #not py24 compliant + values = np.asarray(values) + ss = np.where(values>0, 1, -1) + + sign_map = {(-1, True):"-"} + signs = [sign_map.get((s, v!=0), "") for s, v in zip(ss, values)] + + factor, number_fraction = self._get_number_fraction(factor) + + values = np.abs(values) + + if number_fraction is not None: + values, frac_part = divmod(values, 10**number_fraction) + frac_fmt = "%%0%dd" % (number_fraction,) + frac_str = [frac_fmt % (f1,) for f1 in frac_part] + + if factor == 1: + if number_fraction is None: + return [self.fmt_d % (s*int(v),) for (s, v) in zip(ss, values)] + else: + return [self.fmt_ds % (s*int(v), f1) + for (s, v, f1) in zip(ss, values, frac_str)] + elif factor == 60: + deg_part, min_part = divmod(values, 60) + if number_fraction is None: + return [self.fmt_d_m % (s1, d1, m1) + for s1, d1, m1 in zip(signs, deg_part, min_part)] + else: + return [self.fmt_d_ms % (s, d1, m1, f1) + for s, d1, m1, f1 in zip(signs, deg_part, min_part, frac_str)] + + elif factor == 3600: + if ss[-1] == -1: + inverse_order = True + values = values[::-1] + signs = signs[::-1] + else: + inverse_order = False + + l_hm_old = "" + r = [] + + deg_part, min_part_ = divmod(values, 3600) + min_part, sec_part = divmod(min_part_, 60) + + if number_fraction is None: + sec_str = [self.fmt_s_partial % (s1,) for s1 in sec_part] + else: + sec_str = [self.fmt_ss_partial % (s1, f1) for s1, f1 in zip(sec_part, frac_str)] + + for s, d1, m1, s1 in zip(signs, deg_part, min_part, sec_str): + l_hm = self.fmt_d_m_partial % (s, d1, m1) + if l_hm != l_hm_old: + l_hm_old = l_hm + l = l_hm + s1 #l_s + else: + l = "$" + s + s1 + r.append(l) + + if inverse_order: + return r[::-1] + else: + return r + + else: # factor > 3600. + return [r"$%s^{\circ}$" % (str(v),) for v in ss*values] + + +class FormatterHMS(FormatterDMS): + deg_mark = r"^\mathrm{h}" + min_mark = r"^\mathrm{m}" + sec_mark = r"^\mathrm{s}" + + fmt_d = "$%d" + deg_mark + "$" + fmt_ds = r"$%d.%s" + deg_mark + "$" + + # %s for sign + fmt_d_m = r"$%s%d" + deg_mark + r"\,%02d" + min_mark+"$" + fmt_d_ms = r"$%s%d" + deg_mark + r"\,%02d.%s" + min_mark+"$" + + fmt_d_m_partial = "$%s%d" + deg_mark + r"\,%02d" + min_mark + r"\," + fmt_s_partial = "%02d" + sec_mark + "$" + fmt_ss_partial = "%02d.%s" + sec_mark + "$" + + def __call__(self, direction, factor, values): # hour + return FormatterDMS.__call__(self, direction, factor, np.asarray(values)/15.) + + + + + +class ExtremeFinderCycle(ExtremeFinderSimple): + """ + When there is a cycle, e.g., longitude goes from 0-360. + """ + def __init__(self, + nx, ny, + lon_cycle = 360., + lat_cycle = None, + lon_minmax = None, + lat_minmax = (-90, 90) + ): + #self.transfrom_xy = transform_xy + #self.inv_transfrom_xy = inv_transform_xy + self.nx, self.ny = nx, ny + self.lon_cycle, self.lat_cycle = lon_cycle, lat_cycle + self.lon_minmax = lon_minmax + self.lat_minmax = lat_minmax + + + def __call__(self, transform_xy, x1, y1, x2, y2): + """ + get extreme values. + + x1, y1, x2, y2 in image coordinates (0-based) + nx, ny : number of divisions in each axis + """ + x_, y_ = np.linspace(x1, x2, self.nx), np.linspace(y1, y2, self.ny) + x, y = np.meshgrid(x_, y_) + lon, lat = transform_xy(np.ravel(x), np.ravel(y)) + + # iron out jumps, but algorithm should be improved. + # This is just naive way of doing and my fail for some cases. + # Consider replacing this with numpy.unwrap + # We are ignoring invalid warnings. They are triggered when + # comparing arrays with NaNs using > We are already handling + # that correctly using np.nanmin and np.nanmax + with np.errstate(invalid='ignore'): + if self.lon_cycle is not None: + lon0 = np.nanmin(lon) + lon -= 360. * ((lon - lon0) > 180.) + if self.lat_cycle is not None: + lat0 = np.nanmin(lat) + lat -= 360. * ((lat - lat0) > 180.) + + lon_min, lon_max = np.nanmin(lon), np.nanmax(lon) + lat_min, lat_max = np.nanmin(lat), np.nanmax(lat) + + lon_min, lon_max, lat_min, lat_max = \ + self._adjust_extremes(lon_min, lon_max, lat_min, lat_max) + + return lon_min, lon_max, lat_min, lat_max + + + def _adjust_extremes(self, lon_min, lon_max, lat_min, lat_max): + + lon_min, lon_max, lat_min, lat_max = \ + self._add_pad(lon_min, lon_max, lat_min, lat_max) + + # check cycle + if self.lon_cycle: + lon_max = min(lon_max, lon_min + self.lon_cycle) + if self.lat_cycle: + lat_max = min(lat_max, lat_min + self.lat_cycle) + + if self.lon_minmax is not None: + min0 = self.lon_minmax[0] + lon_min = max(min0, lon_min) + max0 = self.lon_minmax[1] + lon_max = min(max0, lon_max) + + if self.lat_minmax is not None: + min0 = self.lat_minmax[0] + lat_min = max(min0, lat_min) + max0 = self.lat_minmax[1] + lat_max = min(max0, lat_max) + + return lon_min, lon_max, lat_min, lat_max diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/axes_divider.py b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/axes_divider.py new file mode 100644 index 0000000000..5294940530 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/axes_divider.py @@ -0,0 +1,9 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +from mpl_toolkits.axes_grid1.axes_divider import ( + Divider, AxesLocator, SubplotDivider, AxesDivider, locatable_axes_factory, + make_axes_locatable) + +from mpl_toolkits.axes_grid.axislines import Axes +LocatableAxes = locatable_axes_factory(Axes) diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/axes_grid.py b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/axes_grid.py new file mode 100644 index 0000000000..58212ac89c --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/axes_grid.py @@ -0,0 +1,30 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import mpl_toolkits.axes_grid1.axes_grid as axes_grid_orig +from .axes_divider import LocatableAxes + +class CbarAxes(axes_grid_orig.CbarAxesBase, LocatableAxes): + def __init__(self, *kl, **kwargs): + orientation=kwargs.pop("orientation", None) + if orientation is None: + raise ValueError("orientation must be specified") + self.orientation = orientation + self._default_label_on = False + self.locator = None + + super(LocatableAxes, self).__init__(*kl, **kwargs) + + def cla(self): + super(LocatableAxes, self).cla() + self._config_axes() + + +class Grid(axes_grid_orig.Grid): + _defaultLocatableAxesClass = LocatableAxes + +class ImageGrid(axes_grid_orig.ImageGrid): + _defaultLocatableAxesClass = LocatableAxes + _defaultCbarAxesClass = CbarAxes + +AxesGrid = ImageGrid diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/axes_rgb.py b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/axes_rgb.py new file mode 100644 index 0000000000..695a362b57 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/axes_rgb.py @@ -0,0 +1,11 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +from mpl_toolkits.axes_grid1.axes_rgb import ( + make_rgb_axes, imshow_rgb, RGBAxesBase) + +from .axislines import Axes + + +class RGBAxes(RGBAxesBase): + _defaultAxesClass = Axes diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/axis_artist.py b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/axis_artist.py new file mode 100644 index 0000000000..620232112c --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/axis_artist.py @@ -0,0 +1,1527 @@ +""" +axis_artist.py module provides axis-related artists. They are + + * axis line + * tick lines + * tick labels + * axis label + * grid lines + +The main artist class is a AxisArtist and a GridlinesCollection. The +GridlinesCollection is responsible for drawing grid lines and the +AxisArtist is responsible for all other artists. The AxisArtist class +has attributes that are associated with each type of artists. + + * line : axis line + * major_ticks : major tick lines + * major_ticklabels : major tick labels + * minor_ticks : minor tick lines + * minor_ticklabels : minor tick labels + * label : axis label + +Typically, the AxisArtist associated with a axes will be accessed with +the *axis* dictionary of the axes, i.e., the AxisArtist for the bottom +axis is + + ax.axis["bottom"] + +where *ax* is an instance of axes (mpl_toolkits.axislines.Axes). Thus, +ax.axis["bottom"].line is an artist associated with the axis line, and +ax.axis["bottom"].major_ticks is an artist associated with the major tick +lines. + +You can change the colors, fonts, line widths, etc. of these artists +by calling suitable set method. For example, to change the color of the major +ticks of the bottom axis to red, + + ax.axis["bottom"].major_ticks.set_color("r") + +However, things like the locations of ticks, and their ticklabels need +to be changed from the side of the grid_helper. + +axis_direction +-------------- + +AxisArtist, AxisLabel, TickLabels have *axis_direction* attribute, +which adjusts the location, angle, etc.,. The *axis_direction* must be +one of [left, right, bottom, top] and they follow the matplotlib +convention for the rectangle axis. + +For example, for the *bottom* axis (the left and right is relative to +the direction of the increasing coordinate), + + * ticklabels and axislabel are on the right + * ticklabels and axislabel have text angle of 0 + * ticklabels are baseline, center-aligned + * axislabel is top, center-aligned + + +The text angles are actually relative to (90 + angle of the direction +to the ticklabel), which gives 0 for bottom axis. + + Parameter left bottom right top + ticklabels location left right right left + axislabel location left right right left + ticklabels angle 90 0 -90 180 + axislabel angle 180 0 0 180 + ticklabel va center baseline center baseline + axislabel va center top center bottom + ticklabel ha right center right center + axislabel ha right center right center + + +Ticks are by default direct opposite side of the ticklabels. To make +ticks to the same side of the ticklabels, + + ax.axis["bottom"].major_ticks.set_ticks_out(True) + + +Following attributes can be customized (use set_xxx method) + + * Ticks : ticksize, tick_out + * TickLabels : pad + * AxisLabel : pad + +""" +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six + +# FIXME : +# angles are given in data coordinate - need to convert it to canvas coordinate + + +import matplotlib.artist as martist +import matplotlib.text as mtext +import matplotlib.font_manager as font_manager + +from matplotlib.path import Path +from matplotlib.transforms import ( + Affine2D, Bbox, IdentityTransform, ScaledTranslation, TransformedPath) +from matplotlib.collections import LineCollection + +from matplotlib import rcParams + +from matplotlib.artist import allow_rasterization + +import warnings + +import numpy as np + + +import matplotlib.lines as mlines +from .axisline_style import AxislineStyle + + +class BezierPath(mlines.Line2D): + + def __init__(self, path, *kl, **kw): + mlines.Line2D.__init__(self, [], [], *kl, **kw) + self._path = path + self._invalid = False + + def recache(self): + + self._transformed_path = TransformedPath(self._path, self.get_transform()) + + self._invalid = False + + def set_path(self, path): + self._path = path + self._invalid = True + + + def draw(self, renderer): + if self._invalid: + self.recache() + + if not self._visible: return + renderer.open_group('line2d') + + gc = renderer.new_gc() + self._set_gc_clip(gc) + + gc.set_foreground(self._color) + gc.set_antialiased(self._antialiased) + gc.set_linewidth(self._linewidth) + gc.set_alpha(self._alpha) + if self.is_dashed(): + cap = self._dashcapstyle + join = self._dashjoinstyle + else: + cap = self._solidcapstyle + join = self._solidjoinstyle + gc.set_joinstyle(join) + gc.set_capstyle(cap) + gc.set_dashes(self._dashOffset, self._dashSeq) + + if self._lineStyles[self._linestyle] != '_draw_nothing': + tpath, affine = ( + self._transformed_path.get_transformed_path_and_affine()) + renderer.draw_path(gc, tpath, affine.frozen()) + + gc.restore() + renderer.close_group('line2d') + + + +class UnimplementedException(Exception): + pass + +from matplotlib.artist import Artist + +class AttributeCopier(object): + def __init__(self, ref_artist, klass=Artist): + self._klass = klass + self._ref_artist = ref_artist + super(AttributeCopier, self).__init__() + + def set_ref_artist(self, artist): + self._ref_artist = artist + + def get_ref_artist(self): + raise RuntimeError("get_ref_artist must overridden") + #return self._ref_artist + + def get_attribute_from_ref_artist(self, attr_name, default_value): + get_attr_method_name = "get_"+attr_name + c = getattr(self._klass, get_attr_method_name)(self) + if c == 'auto': + ref_artist = self.get_ref_artist() + if ref_artist: + attr = getattr(ref_artist, + get_attr_method_name)() + return attr + else: + return default_value + + return c + + +from matplotlib.lines import Line2D + +class Ticks(Line2D, AttributeCopier): + """ + Ticks are derived from Line2D, and note that ticks themselves + are markers. Thus, you should use set_mec, set_mew, etc. + + To change the tick size (length), you need to use + set_ticksize. To change the direction of the ticks (ticks are + in opposite direction of ticklabels by default), use + set_tick_out(False). + """ + + def __init__(self, ticksize, tick_out=False, **kwargs): + self._ticksize = ticksize + self.locs_angles_labels = [] + + self.set_tick_out(tick_out) + + self._axis = kwargs.pop("axis", None) + if self._axis is not None: + if "color" not in kwargs: + kwargs["color"] = "auto" + if ("mew" not in kwargs) and ("markeredgewidth" not in kwargs): + kwargs["markeredgewidth"] = "auto" + + Line2D.__init__(self, [0.], [0.], **kwargs) + AttributeCopier.__init__(self, self._axis, klass=Line2D) + self.set_snap(True) + + def get_ref_artist(self): + #return self._ref_artist.get_ticklines()[0] + return self._ref_artist.majorTicks[0].tick1line + + def get_color(self): + return self.get_attribute_from_ref_artist("color", "k") + + def get_markeredgecolor(self): + if self._markeredgecolor == 'auto': + return self.get_color() + else: + return self._markeredgecolor + + def get_markeredgewidth(self): + return self.get_attribute_from_ref_artist("markeredgewidth", .5) + + + def set_tick_out(self, b): + """ + set True if tick need to be rotated by 180 degree. + """ + self._tick_out = b + + def get_tick_out(self): + """ + Return True if the tick will be rotated by 180 degree. + """ + return self._tick_out + + + def set_ticksize(self, ticksize): + """ + set length of the ticks in points. + """ + self._ticksize = ticksize + + + def get_ticksize(self): + """ + Return length of the ticks in points. + """ + return self._ticksize + + def set_locs_angles(self, locs_angles): + self.locs_angles = locs_angles + + + def _update(self, renderer): + pass + + _tickvert_path = Path([[0., 0.], [1., 0.]]) + + def draw(self, renderer): + if not self.get_visible(): + return + + self._update(renderer) # update the tick + + size = self._ticksize + path_trans = self.get_transform() + + # set gc : copied from lines.py +# gc = renderer.new_gc() +# self._set_gc_clip(gc) + +# gc.set_foreground(self.get_color()) +# gc.set_antialiased(self._antialiased) +# gc.set_linewidth(self._linewidth) +# gc.set_alpha(self._alpha) +# if self.is_dashed(): +# cap = self._dashcapstyle +# join = self._dashjoinstyle +# else: +# cap = self._solidcapstyle +# join = self._solidjoinstyle +# gc.set_joinstyle(join) +# gc.set_capstyle(cap) +# gc.set_snap(self.get_snap()) + + + gc = renderer.new_gc() + gc.set_foreground(self.get_markeredgecolor()) + gc.set_linewidth(self.get_markeredgewidth()) + gc.set_alpha(self._alpha) + + offset = renderer.points_to_pixels(size) + marker_scale = Affine2D().scale(offset, offset) + + if self.get_tick_out(): + add_angle = 180 + else: + add_angle = 0 + + marker_rotation = Affine2D() + marker_transform = marker_scale + marker_rotation + + for loc, angle in self.locs_angles: + marker_rotation.clear().rotate_deg(angle+add_angle) + locs = path_trans.transform_non_affine(np.array([loc])) + if self.axes and not self.axes.viewLim.contains(*locs[0]): + continue + renderer.draw_markers(gc, self._tickvert_path, marker_transform, + Path(locs), path_trans.get_affine()) + + gc.restore() + + +class LabelBase(mtext.Text): + """ + A base class for AxisLabel and TickLabels. The position and angle + of the text are calculated by to offset_ref_angle, + text_ref_angle, and offset_radius attributes. + """ + + def __init__(self, *kl, **kwargs): + self.locs_angles_labels = [] + self._ref_angle = 0 + self._offset_radius = 0. + + super(LabelBase, self).__init__(*kl, + **kwargs) + + self.set_rotation_mode("anchor") + self._text_follow_ref_angle = True + #self._offset_ref_angle = 0 + + def _set_ref_angle(self, a): + self._ref_angle = a + + def _get_ref_angle(self): + return self._ref_angle + + def _get_text_ref_angle(self): + if self._text_follow_ref_angle: + return self._get_ref_angle()+90 + else: + return 0 #self.get_ref_angle() + + def _get_offset_ref_angle(self): + return self._get_ref_angle() + + def _set_offset_radius(self, offset_radius): + self._offset_radius = offset_radius + + def _get_offset_radius(self): + return self._offset_radius + + + _get_opposite_direction = {"left":"right", + "right":"left", + "top":"bottom", + "bottom":"top"}.__getitem__ + + + def _update(self, renderer): + pass + + def draw(self, renderer): + if not self.get_visible(): return + + self._update(renderer) + + # save original and adjust some properties + tr = self.get_transform() + angle_orig = self.get_rotation() + + offset_tr = Affine2D() + self.set_transform(tr+offset_tr) + + text_ref_angle = self._get_text_ref_angle() + offset_ref_angle = self._get_offset_ref_angle() + + theta = (offset_ref_angle)/180.*np.pi + dd = self._get_offset_radius() + dx, dy = dd * np.cos(theta), dd * np.sin(theta) + offset_tr.translate(dx, dy) + self.set_rotation(text_ref_angle+angle_orig) + super(LabelBase, self).draw(renderer) + offset_tr.clear() + + + # restore original properties + self.set_transform(tr) + self.set_rotation(angle_orig) + + + def get_window_extent(self, renderer): + + self._update(renderer) + + # save original and adjust some properties + tr = self.get_transform() + angle_orig = self.get_rotation() + + offset_tr = Affine2D() + self.set_transform(tr+offset_tr) + + text_ref_angle = self._get_text_ref_angle() + offset_ref_angle = self._get_offset_ref_angle() + + theta = (offset_ref_angle)/180.*np.pi + dd = self._get_offset_radius() + dx, dy = dd * np.cos(theta), dd * np.sin(theta) + offset_tr.translate(dx, dy) + self.set_rotation(text_ref_angle+angle_orig) + + bbox = super(LabelBase, self).get_window_extent(renderer).frozen() + + offset_tr.clear() + + + # restore original properties + self.set_transform(tr) + self.set_rotation(angle_orig) + + return bbox + + +class AxisLabel(LabelBase, AttributeCopier): + """ + Axis Label. Derived from Text. The position of the text is updated + in the fly, so changing text position has no effect. Otherwise, the + properties can be changed as a normal Text. + + To change the pad between ticklabels and axis label, use set_pad. + """ + + def __init__(self, *kl, **kwargs): + + axis_direction = kwargs.pop("axis_direction", "bottom") + self._axis = kwargs.pop("axis", None) + #super(AxisLabel, self).__init__(*kl, **kwargs) + LabelBase.__init__(self, *kl, **kwargs) + AttributeCopier.__init__(self, self._axis, klass=LabelBase) + + self.set_axis_direction(axis_direction) + self._pad = 5 + self._extra_pad = 0 + + def set_pad(self, pad): + """ + Set the pad in points. Note that the actual pad will be the + sum of the internal pad and the external pad (that are set + automatically by the AxisArtist), and it only set the internal + pad + """ + self._pad = pad + + def get_pad(self): + """ + return pad in points. See set_pad for more details. + """ + return self._pad + + + def _set_external_pad(self, p): + """ + Set external pad IN PIXELS. This is intended to be set by the + AxisArtist, bot by user.. + """ + self._extra_pad = p + + def _get_external_pad(self): + """ + Get external pad. + """ + return self._extra_pad + + + def get_ref_artist(self): + return self._axis.get_label() + + + def get_text(self): + t = super(AxisLabel, self).get_text() + if t == "__from_axes__": + return self._axis.get_label().get_text() + return self._text + + _default_alignments = dict(left=("bottom", "center"), + right=("top", "center"), + bottom=("top", "center"), + top=("bottom", "center")) + + + + def set_default_alignment(self, d): + if d not in ["left", "right", "top", "bottom"]: + raise ValueError('direction must be on of "left", "right", "top", "bottom"') + + va, ha = self._default_alignments[d] + self.set_va(va) + self.set_ha(ha) + + + _default_angles = dict(left=180, + right=0, + bottom=0, + top=180) + + + def set_default_angle(self, d): + if d not in ["left", "right", "top", "bottom"]: + raise ValueError('direction must be on of "left", "right", "top", "bottom"') + + self.set_rotation(self._default_angles[d]) + + + def set_axis_direction(self, d): + """ + Adjust the text angle and text alignment of axis label + according to the matplotlib convention. + + + ===================== ========== ========= ========== ========== + property left bottom right top + ===================== ========== ========= ========== ========== + axislabel angle 180 0 0 180 + axislabel va center top center bottom + axislabel ha right center right center + ===================== ========== ========= ========== ========== + + Note that the text angles are actually relative to (90 + angle + of the direction to the ticklabel), which gives 0 for bottom + axis. + + """ + if d not in ["left", "right", "top", "bottom"]: + raise ValueError('direction must be on of "left", "right", "top", "bottom"') + + self.set_default_alignment(d) + self.set_default_angle(d) + + def get_color(self): + return self.get_attribute_from_ref_artist("color", "k") + + def draw(self, renderer): + if not self.get_visible(): + return + + pad = renderer.points_to_pixels(self.get_pad()) + r = self._get_external_pad() + pad + self._set_offset_radius(r) + + super(AxisLabel, self).draw(renderer) + + + def get_window_extent(self, renderer): + + if not self.get_visible(): + return + + pad = renderer.points_to_pixels(self.get_pad()) + r = self._get_external_pad() + pad + self._set_offset_radius(r) + + bb = super(AxisLabel, self).get_window_extent(renderer) + + return bb + + +class TickLabels(AxisLabel, AttributeCopier): # mtext.Text + """ + Tick Labels. While derived from Text, this single artist draws all + ticklabels. As in AxisLabel, the position of the text is updated + in the fly, so changing text position has no effect. Otherwise, + the properties can be changed as a normal Text. Unlike the + ticklabels of the mainline matplotlib, properties of single + ticklabel alone cannot modified. + + To change the pad between ticks and ticklabels, use set_pad. + """ + + def __init__(self, **kwargs): + + axis_direction = kwargs.pop("axis_direction", "bottom") + AxisLabel.__init__(self, **kwargs) + self.set_axis_direction(axis_direction) + #self._axis_direction = axis_direction + self._axislabel_pad = 0 + #self._extra_pad = 0 + + + # attribute copier + def get_ref_artist(self): + return self._axis.get_ticklabels()[0] + + def set_axis_direction(self, label_direction): + """ + Adjust the text angle and text alignment of ticklabels + according to the matplotlib convention. + + The *label_direction* must be one of [left, right, bottom, + top]. + + ===================== ========== ========= ========== ========== + property left bottom right top + ===================== ========== ========= ========== ========== + ticklabels angle 90 0 -90 180 + ticklabel va center baseline center baseline + ticklabel ha right center right center + ===================== ========== ========= ========== ========== + + + Note that the text angles are actually relative to (90 + angle + of the direction to the ticklabel), which gives 0 for bottom + axis. + + """ + + if label_direction not in ["left", "right", "top", "bottom"]: + raise ValueError('direction must be one of "left", "right", "top", "bottom"') + + self._axis_direction = label_direction + self.set_default_alignment(label_direction) + self.set_default_angle(label_direction) + + + def invert_axis_direction(self): + label_direction = self._get_opposite_direction(self._axis_direction) + self.set_axis_direction(label_direction) + + def _get_ticklabels_offsets(self, renderer, label_direction): + """ + Calculates the offsets of the ticklabels from the tick and + their total heights. The offset only takes account the offset + due to the vertical alignment of the ticklabels, i.e.,if axis + direction is bottom and va is ;top', it will return 0. if va + is 'baseline', it will return (height-descent). + """ + whd_list = self.get_texts_widths_heights_descents(renderer) + + if not whd_list: + return 0, 0 + + r = 0 + va, ha = self.get_va(), self.get_ha() + + if label_direction == "left": + pad = max(w for w, h, d in whd_list) + if ha == "left": + r = pad + elif ha == "center": + r = .5 * pad + elif label_direction == "right": + pad = max(w for w, h, d in whd_list) + if ha == "right": + r = pad + elif ha == "center": + r = .5 * pad + elif label_direction == "bottom": + pad = max(h for w, h, d in whd_list) + if va == "bottom": + r = pad + elif va == "center": + r =.5 * pad + elif va == "baseline": + max_ascent = max(h - d for w, h, d in whd_list) + max_descent = max(d for w, h, d in whd_list) + r = max_ascent + pad = max_ascent + max_descent + elif label_direction == "top": + pad = max(h for w, h, d in whd_list) + if va == "top": + r = pad + elif va == "center": + r =.5 * pad + elif va == "baseline": + max_ascent = max(h - d for w, h, d in whd_list) + max_descent = max(d for w, h, d in whd_list) + r = max_descent + pad = max_ascent + max_descent + + #tick_pad = renderer.points_to_pixels(self.get_pad()) + + # r : offset + + # pad : total height of the ticklabels. This will be used to + # calculate the pad for the axislabel. + return r, pad + + + + _default_alignments = dict(left=("center", "right"), + right=("center", "left"), + bottom=("baseline", "center"), + top=("baseline", "center")) + + + + # set_default_alignments(self, d) + + _default_angles = dict(left=90, + right=-90, + bottom=0, + top=180) + + + def draw(self, renderer): + if not self.get_visible(): + self._axislabel_pad = self._get_external_pad() + return + + r, total_width = self._get_ticklabels_offsets(renderer, + self._axis_direction) + + #self._set_external_pad(r+self._get_external_pad()) + pad = self._get_external_pad() + \ + renderer.points_to_pixels(self.get_pad()) + self._set_offset_radius(r+pad) + + #self._set_offset_radius(r) + + for (x, y), a, l in self._locs_angles_labels: + if not l.strip(): continue + self._set_ref_angle(a) #+ add_angle + self.set_x(x) + self.set_y(y) + self.set_text(l) + LabelBase.draw(self, renderer) + + self._axislabel_pad = total_width \ + + pad # the value saved will be used to draw axislabel. + + + def set_locs_angles_labels(self, locs_angles_labels): + self._locs_angles_labels = locs_angles_labels + + def get_window_extents(self, renderer): + + if not self.get_visible(): + self._axislabel_pad = self._get_external_pad() + return [] + + bboxes = [] + + r, total_width = self._get_ticklabels_offsets(renderer, + self._axis_direction) + + pad = self._get_external_pad() + \ + renderer.points_to_pixels(self.get_pad()) + self._set_offset_radius(r+pad) + + + for (x, y), a, l in self._locs_angles_labels: + self._set_ref_angle(a) #+ add_angle + self.set_x(x) + self.set_y(y) + self.set_text(l) + bb = LabelBase.get_window_extent(self, renderer) + bboxes.append(bb) + + self._axislabel_pad = total_width \ + + pad # the value saved will be used to draw axislabel. + + return bboxes + + + def get_texts_widths_heights_descents(self, renderer): + """ + return a list of width, height, descent for ticklabels. + """ + whd_list = [] + for (x, y), a, l in self._locs_angles_labels: + if not l.strip(): continue + clean_line, ismath = self.is_math_text(l) + whd = renderer.get_text_width_height_descent( + clean_line, self._fontproperties, ismath=ismath) + whd_list.append(whd) + + return whd_list + + +class GridlinesCollection(LineCollection): + def __init__(self, *kl, **kwargs): + """ + *which* : "major" or "minor" + *axis* : "both", "x" or "y" + """ + self._which = kwargs.pop("which", "major") + self._axis = kwargs.pop("axis", "both") + super(GridlinesCollection, self).__init__(*kl, **kwargs) + self.set_grid_helper(None) + + def set_which(self, which): + self._which = which + + def set_axis(self, axis): + self._axis = axis + + def set_grid_helper(self, grid_helper): + self._grid_helper = grid_helper + + def draw(self, renderer): + if self._grid_helper is not None: + self._grid_helper.update_lim(self.axes) + gl = self._grid_helper.get_gridlines(self._which, self._axis) + if gl: + self.set_segments([np.transpose(l) for l in gl]) + else: + self.set_segments([]) + super(GridlinesCollection, self).draw(renderer) + + + + +class AxisArtist(martist.Artist): + """ + An artist which draws axis (a line along which the n-th axes coord + is constant) line, ticks, ticklabels, and axis label. + """ + + ZORDER=2.5 + + @property + def LABELPAD(self): + return self.label.get_pad() + + @LABELPAD.setter + def LABELPAD(self, v): + return self.label.set_pad(v) + + def __init__(self, axes, + helper, + offset=None, + axis_direction="bottom", + **kw): + """ + *axes* : axes + *helper* : an AxisArtistHelper instance. + """ + #axes is also used to follow the axis attribute (tick color, etc). + + super(AxisArtist, self).__init__(**kw) + + self.axes = axes + + self._axis_artist_helper = helper + + if offset is None: + offset = (0, 0) + self.dpi_transform = Affine2D() + self.offset_transform = ScaledTranslation(offset[0], offset[1], + self.dpi_transform) + + self._label_visible = True + self._majortick_visible = True + self._majorticklabel_visible = True + self._minortick_visible = True + self._minorticklabel_visible = True + + + #if self._axis_artist_helper._loc in ["left", "right"]: + if axis_direction in ["left", "right"]: + axis_name = "ytick" + self.axis = axes.yaxis + else: + axis_name = "xtick" + self.axis = axes.xaxis + + + self._axisline_style = None + + + self._axis_direction = axis_direction + + + self._init_line() + self._init_ticks(axis_name, **kw) + self._init_offsetText(axis_direction) + self._init_label() + + self.set_zorder(self.ZORDER) + + self._rotate_label_along_line = False + + # axis direction + self._tick_add_angle = 180. + self._ticklabel_add_angle = 0. + self._axislabel_add_angle = 0. + self.set_axis_direction(axis_direction) + + + # axis direction + + def set_axis_direction(self, axis_direction): + """ + Adjust the direction, text angle, text alignment of + ticklabels, labels following the matplotlib convention for + the rectangle axes. + + The *axis_direction* must be one of [left, right, bottom, + top]. + + ===================== ========== ========= ========== ========== + property left bottom right top + ===================== ========== ========= ========== ========== + ticklabels location "-" "+" "+" "-" + axislabel location "-" "+" "+" "-" + ticklabels angle 90 0 -90 180 + ticklabel va center baseline center baseline + ticklabel ha right center right center + axislabel angle 180 0 0 180 + axislabel va center top center bottom + axislabel ha right center right center + ===================== ========== ========= ========== ========== + + + Note that the direction "+" and "-" are relative to the direction of + the increasing coordinate. Also, the text angles are actually + relative to (90 + angle of the direction to the ticklabel), + which gives 0 for bottom axis. + + """ + + if axis_direction not in ["left", "right", "top", "bottom"]: + raise ValueError('direction must be on of "left", "right", "top", "bottom"') + self._axis_direction = axis_direction + if axis_direction in ["left", "top"]: + #self._set_tick_direction("+") + self.set_ticklabel_direction("-") + self.set_axislabel_direction("-") + else: + #self._set_tick_direction("-") + self.set_ticklabel_direction("+") + self.set_axislabel_direction("+") + + self.major_ticklabels.set_axis_direction(axis_direction) + self.label.set_axis_direction(axis_direction) + + # def _set_tick_direction(self, d): + # if d not in ["+", "-"]: + # raise ValueError('direction must be on of "in", "out"') + + # if d == "+": + # self._tick_add_angle = 0 #get_helper()._extremes=0, 10 + # else: + # self._tick_add_angle = 180 #get_helper()._extremes=0, 10 + + def set_ticklabel_direction(self, tick_direction): + """ + Adjust the direction of the ticklabel. + + ACCEPTS: [ "+" | "-" ] + + Note that the label_direction '+' and '-' are relative to the + direction of the increasing coordinate. + """ + + if tick_direction not in ["+", "-"]: + raise ValueError('direction must be one of "+", "-"') + + if tick_direction == "-": + self._ticklabel_add_angle = 180 + else: + self._ticklabel_add_angle = 0 + + def invert_ticklabel_direction(self): + self._ticklabel_add_angle = (self._ticklabel_add_angle + 180) % 360 + self.major_ticklabels.invert_axis_direction() + self.minor_ticklabels.invert_axis_direction() + + # def invert_ticks_direction(self): + # self.major_ticks.set_tick_out(not self.major_ticks.get_tick_out()) + # self.minor_ticks.set_tick_out(not self.minor_ticks.get_tick_out()) + + def set_axislabel_direction(self, label_direction): + """ + Adjust the direction of the axislabel. + + ACCEPTS: [ "+" | "-" ] + + Note that the label_direction '+' and '-' are relative to the + direction of the increasing coordinate. + """ + if label_direction not in ["+", "-"]: + raise ValueError('direction must be one of "+", "-"') + + if label_direction == "-": + self._axislabel_add_angle = 180 + else: + self._axislabel_add_angle = 0 + + + + def get_transform(self): + return self.axes.transAxes + self.offset_transform + + def get_helper(self): + """ + Return axis artist helper instance. + """ + return self._axis_artist_helper + + + def set_axisline_style(self, axisline_style=None, **kw): + """ + Set the axisline style. + + *axisline_style* can be a string with axisline style name with optional + comma-separated attributes. Alternatively, the attrs can + be provided as keywords. + + set_arrowstyle("->,size=1.5") + set_arrowstyle("->", size=1.5) + + Old attrs simply are forgotten. + + Without argument (or with arrowstyle=None), return + available styles as a list of strings. + """ + + if axisline_style==None: + return AxislineStyle.pprint_styles() + + if isinstance(axisline_style, AxislineStyle._Base): + self._axisline_style = axisline_style + else: + self._axisline_style = AxislineStyle(axisline_style, **kw) + + + self._init_line() + + + def get_axisline_style(self): + """ + return the current axisline style. + """ + return self._axisline_style + + def _init_line(self): + """ + Initialize the *line* artist that is responsible to draw the axis line. + """ + tran = self._axis_artist_helper.get_line_transform(self.axes) \ + + self.offset_transform + + axisline_style = self.get_axisline_style() + if axisline_style is None: + self.line = BezierPath(self._axis_artist_helper.get_line(self.axes), + color=rcParams['axes.edgecolor'], + linewidth=rcParams['axes.linewidth'], + transform=tran) + else: + self.line = axisline_style(self, transform=tran) + + def _draw_line(self, renderer): + self.line.set_path(self._axis_artist_helper.get_line(self.axes)) + if self.get_axisline_style() is not None: + self.line.set_line_mutation_scale(self.major_ticklabels.get_size()) + self.line.draw(renderer) + + + def _init_ticks(self, axis_name, **kw): + + trans=self._axis_artist_helper.get_tick_transform(self.axes) \ + + self.offset_transform + + + major_tick_size = kw.get("major_tick_size", + rcParams['%s.major.size'%axis_name]) + major_tick_pad = kw.get("major_tick_pad", + rcParams['%s.major.pad'%axis_name]) + minor_tick_size = kw.get("minor_tick_size", + rcParams['%s.minor.size'%axis_name]) + minor_tick_pad = kw.get("minor_tick_pad", + rcParams['%s.minor.pad'%axis_name]) + + self.major_ticks = Ticks(major_tick_size, + axis=self.axis, + transform=trans) + self.minor_ticks = Ticks(minor_tick_size, + axis=self.axis, + transform=trans) + + if axis_name == "xaxis": + size = rcParams['xtick.labelsize'] + else: + size = rcParams['ytick.labelsize'] + + + fontprops = font_manager.FontProperties(size=size) + + self.major_ticklabels = TickLabels(size=size, axis=self.axis, + axis_direction=self._axis_direction) + self.minor_ticklabels = TickLabels(size=size, axis=self.axis, + axis_direction=self._axis_direction) + + + self.major_ticklabels.set(figure = self.axes.figure, + transform=trans, + fontproperties=fontprops) + self.major_ticklabels.set_pad(major_tick_pad) + + self.minor_ticklabels.set(figure = self.axes.figure, + transform=trans, + fontproperties=fontprops) + self.minor_ticklabels.set_pad(minor_tick_pad) + + + + def _get_tick_info(self, tick_iter): + """ + return ticks_loc_angle, ticklabels_loc_angle_label + + ticks_loc_angle : list of locs and angles for ticks + ticklabels_loc_angle_label : list of locs, angles and labels for tickslabels + """ + ticks_loc_angle = [] + ticklabels_loc_angle_label = [] + + tick_add_angle = self._tick_add_angle + ticklabel_add_angle = self._ticklabel_add_angle + + for loc, angle_normal, angle_tangent, label in tick_iter: + angle_label = angle_tangent - 90 + angle_label += ticklabel_add_angle + + if np.cos((angle_label - angle_normal)/180.*np.pi) < 0.: + angle_tick = angle_normal + else: + angle_tick = angle_normal + 180 + + ticks_loc_angle.append([loc, angle_tick]) + ticklabels_loc_angle_label.append([loc, angle_label, label]) + + return ticks_loc_angle, ticklabels_loc_angle_label + + + def _update_ticks(self, renderer): + + + # set extra pad for major and minor ticklabels: + # use ticksize of majorticks even for minor ticks. not clear what is best. + + dpi_cor = renderer.points_to_pixels(1.) + if self.major_ticks.get_visible() and self.major_ticks.get_tick_out(): + self.major_ticklabels._set_external_pad(self.major_ticks._ticksize*dpi_cor) + self.minor_ticklabels._set_external_pad(self.major_ticks._ticksize*dpi_cor) + else: + self.major_ticklabels._set_external_pad(0) + self.minor_ticklabels._set_external_pad(0) + + + majortick_iter, minortick_iter = \ + self._axis_artist_helper.get_tick_iterators(self.axes) + + tick_loc_angle, ticklabel_loc_angle_label \ + = self._get_tick_info(majortick_iter) + + self.major_ticks.set_locs_angles(tick_loc_angle) + self.major_ticklabels.set_locs_angles_labels(ticklabel_loc_angle_label) + + #self.major_ticks.draw(renderer) + #self.major_ticklabels.draw(renderer) + + + # minor ticks + tick_loc_angle, ticklabel_loc_angle_label \ + = self._get_tick_info(minortick_iter) + + self.minor_ticks.set_locs_angles(tick_loc_angle) + self.minor_ticklabels.set_locs_angles_labels(ticklabel_loc_angle_label) + + #self.minor_ticks.draw(renderer) + #self.minor_ticklabels.draw(renderer) + + + #if (self.major_ticklabels.get_visible() or self.minor_ticklabels.get_visible()): + # self._draw_offsetText(renderer) + + return self.major_ticklabels.get_window_extents(renderer) + + + def _draw_ticks(self, renderer): + + extents = self._update_ticks(renderer) + + self.major_ticks.draw(renderer) + self.major_ticklabels.draw(renderer) + + self.minor_ticks.draw(renderer) + self.minor_ticklabels.draw(renderer) + + + if (self.major_ticklabels.get_visible() or self.minor_ticklabels.get_visible()): + self._draw_offsetText(renderer) + + return extents + + def _draw_ticks2(self, renderer): + + + # set extra pad for major and minor ticklabels: + # use ticksize of majorticks even for minor ticks. not clear what is best. + + dpi_cor = renderer.points_to_pixels(1.) + if self.major_ticks.get_visible() and self.major_ticks.get_tick_out(): + self.major_ticklabels._set_external_pad(self.major_ticks._ticksize*dpi_cor) + self.minor_ticklabels._set_external_pad(self.major_ticks._ticksize*dpi_cor) + else: + self.major_ticklabels._set_external_pad(0) + self.minor_ticklabels._set_external_pad(0) + + + majortick_iter, minortick_iter = \ + self._axis_artist_helper.get_tick_iterators(self.axes) + + tick_loc_angle, ticklabel_loc_angle_label \ + = self._get_tick_info(majortick_iter) + + self.major_ticks.set_locs_angles(tick_loc_angle) + self.major_ticklabels.set_locs_angles_labels(ticklabel_loc_angle_label) + + self.major_ticks.draw(renderer) + self.major_ticklabels.draw(renderer) + + + # minor ticks + tick_loc_angle, ticklabel_loc_angle_label \ + = self._get_tick_info(minortick_iter) + + self.minor_ticks.set_locs_angles(tick_loc_angle) + self.minor_ticklabels.set_locs_angles_labels(ticklabel_loc_angle_label) + + self.minor_ticks.draw(renderer) + self.minor_ticklabels.draw(renderer) + + + if (self.major_ticklabels.get_visible() or self.minor_ticklabels.get_visible()): + self._draw_offsetText(renderer) + + return self.major_ticklabels.get_window_extents(renderer) + + + + + _offsetText_pos = dict(left=(0, 1, "bottom", "right"), + right=(1, 1, "bottom", "left"), + bottom=(1, 0, "top", "right"), + top=(1, 1, "bottom", "right")) + + def _init_offsetText(self, direction): + + x,y,va,ha = self._offsetText_pos[direction] + + self.offsetText = mtext.Annotation("", + xy=(x,y), xycoords="axes fraction", + xytext=(0,0), textcoords="offset points", + #fontproperties = fp, + color = rcParams['xtick.color'], + verticalalignment=va, + horizontalalignment=ha, + ) + self.offsetText.set_transform(IdentityTransform()) + self.axes._set_artist_props(self.offsetText) + + + def _update_offsetText(self): + self.offsetText.set_text( self.axis.major.formatter.get_offset() ) + self.offsetText.set_size(self.major_ticklabels.get_size()) + offset = self.major_ticklabels.get_pad() + self.major_ticklabels.get_size() + 2. + self.offsetText.xyann= (0, offset) + + + def _draw_offsetText(self, renderer): + self._update_offsetText() + self.offsetText.draw(renderer) + + + + def _init_label(self, **kw): + # x in axes coords, y in display coords (to be updated at draw + # time by _update_label_positions) + + labelsize = kw.get("labelsize", + rcParams['axes.labelsize']) + #labelcolor = kw.get("labelcolor", + # rcParams['axes.labelcolor']) + fontprops = font_manager.FontProperties( + size=labelsize, + weight=rcParams['axes.labelweight']) + textprops = dict(fontproperties = fontprops) + #color = labelcolor) + + tr = self._axis_artist_helper.get_axislabel_transform(self.axes) \ + + self.offset_transform + + self.label = AxisLabel(0, 0, "__from_axes__", + color = "auto", #rcParams['axes.labelcolor'], + fontproperties=fontprops, + axis=self.axis, + transform=tr, + axis_direction=self._axis_direction, + ) + + self.label.set_figure(self.axes.figure) + + labelpad = kw.get("labelpad", 5) + self.label.set_pad(labelpad) + + + def _update_label(self, renderer): + + if not self.label.get_visible(): + return + + fontprops = font_manager.FontProperties( + size=rcParams['axes.labelsize'], + weight=rcParams['axes.labelweight']) + + #pad_points = self.major_tick_pad + + #if abs(self._ticklabel_add_angle - self._axislabel_add_angle)%360 > 90: + if self._ticklabel_add_angle != self._axislabel_add_angle: + if (self.major_ticks.get_visible() and not self.major_ticks.get_tick_out()) \ + or \ + (self.minor_ticks.get_visible() and not self.major_ticks.get_tick_out()): + axislabel_pad = self.major_ticks._ticksize + else: + axislabel_pad = 0 + else: + axislabel_pad = max(self.major_ticklabels._axislabel_pad, + self.minor_ticklabels._axislabel_pad) + + + #label_offset = axislabel_pad + self.LABELPAD + + #self.label._set_offset_radius(label_offset) + self.label._set_external_pad(axislabel_pad) + + xy, angle_tangent = self._axis_artist_helper.get_axislabel_pos_angle(self.axes) + if xy is None: return + + angle_label = angle_tangent - 90 + + + x, y = xy + self.label._set_ref_angle(angle_label+self._axislabel_add_angle) + self.label.set(x=x, y=y) + + + def _draw_label(self, renderer): + self._update_label(renderer) + self.label.draw(renderer) + + def _draw_label2(self, renderer): + + if not self.label.get_visible(): + return + + fontprops = font_manager.FontProperties( + size=rcParams['axes.labelsize'], + weight=rcParams['axes.labelweight']) + + #pad_points = self.major_tick_pad + + #if abs(self._ticklabel_add_angle - self._axislabel_add_angle)%360 > 90: + if self._ticklabel_add_angle != self._axislabel_add_angle: + if (self.major_ticks.get_visible() and not self.major_ticks.get_tick_out()) \ + or \ + (self.minor_ticks.get_visible() and not self.major_ticks.get_tick_out()): + axislabel_pad = self.major_ticks._ticksize + else: + axislabel_pad = 0 + else: + axislabel_pad = max(self.major_ticklabels._axislabel_pad, + self.minor_ticklabels._axislabel_pad) + + #label_offset = axislabel_pad + self.LABELPAD + + #self.label._set_offset_radius(label_offset) + self.label._set_external_pad(axislabel_pad) + + xy, angle_tangent = self._axis_artist_helper.get_axislabel_pos_angle(self.axes) + if xy is None: return + + angle_label = angle_tangent - 90 + + x, y = xy + self.label._set_ref_angle(angle_label+self._axislabel_add_angle) + self.label.set(x=x, y=y) + self.label.draw(renderer) + + + + def set_label(self, s): + self.label.set_text(s) + + + + def get_tightbbox(self, renderer): + if not self.get_visible(): return + + self._axis_artist_helper.update_lim(self.axes) + + dpi_cor = renderer.points_to_pixels(1.) + self.dpi_transform.clear().scale(dpi_cor, dpi_cor) + + + bb = [] + + self._update_ticks(renderer) + + #if self.major_ticklabels.get_visible(): + bb.extend(self.major_ticklabels.get_window_extents(renderer)) + #if self.minor_ticklabels.get_visible(): + bb.extend(self.minor_ticklabels.get_window_extents(renderer)) + + + self._update_label(renderer) + + #if self.label.get_visible(): + bb.append(self.label.get_window_extent(renderer)) + bb.append(self.offsetText.get_window_extent(renderer)) + + bb = [b for b in bb if b and (b.width!=0 or b.height!=0)] + if bb: + _bbox = Bbox.union(bb) + return _bbox + else: + return None + + #self._draw_line(renderer) + + #self._draw_ticks(renderer) + + #self._draw_offsetText(renderer) + #self._draw_label(renderer) + + + + @allow_rasterization + def draw(self, renderer): + 'Draw the axis lines, tick lines and labels' + + if not self.get_visible(): return + + renderer.open_group(__name__) + + self._axis_artist_helper.update_lim(self.axes) + + dpi_cor = renderer.points_to_pixels(1.) + self.dpi_transform.clear().scale(dpi_cor, dpi_cor) + + + self._draw_ticks(renderer) + + self._draw_line(renderer) + + #self._draw_offsetText(renderer) + self._draw_label(renderer) + + renderer.close_group(__name__) + + #def get_ticklabel_extents(self, renderer): + # pass + + def toggle(self, all=None, ticks=None, ticklabels=None, label=None): + """ + Toggle visibility of ticks, ticklabels, and (axis) label. + To turn all off, :: + + axis.toggle(all=False) + + To turn all off but ticks on :: + + axis.toggle(all=False, ticks=True) + + To turn all on but (axis) label off :: + + axis.toggle(all=True, label=False)) + + """ + if all: + _ticks, _ticklabels, _label = True, True, True + elif all is not None: + _ticks, _ticklabels, _label = False, False, False + else: + _ticks, _ticklabels, _label = None, None, None + + if ticks is not None: + _ticks = ticks + if ticklabels is not None: + _ticklabels = ticklabels + if label is not None: + _label = label + + if _ticks is not None: + self.major_ticks.set_visible(_ticks) + self.minor_ticks.set_visible(_ticks) + if _ticklabels is not None: + self.major_ticklabels.set_visible(_ticklabels) + self.minor_ticklabels.set_visible(_ticklabels) + if _label is not None: + self.label.set_visible(_label) diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/axisline_style.py b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/axisline_style.py new file mode 100644 index 0000000000..876f5fe189 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/axisline_style.py @@ -0,0 +1,168 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six + +from matplotlib.patches import _Style, FancyArrowPatch +from matplotlib.transforms import IdentityTransform +from matplotlib.path import Path +import numpy as np + +class _FancyAxislineStyle(object): + class SimpleArrow(FancyArrowPatch): + """ + The artist class that will be returned for SimpleArrow style. + """ + _ARROW_STYLE = "->" + + def __init__(self, axis_artist, line_path, transform, + line_mutation_scale): + self._axis_artist = axis_artist + self._line_transform = transform + self._line_path = line_path + self._line_mutation_scale = line_mutation_scale + + FancyArrowPatch.__init__(self, + path=self._line_path, + arrowstyle=self._ARROW_STYLE, + arrow_transmuter=None, + patchA=None, + patchB=None, + shrinkA=0., + shrinkB=0., + mutation_scale=line_mutation_scale, + mutation_aspect=None, + transform=IdentityTransform(), + ) + + def set_line_mutation_scale(self, scale): + self.set_mutation_scale(scale*self._line_mutation_scale) + + def _extend_path(self, path, mutation_size=10): + """ + Extend the path to make a room for drawing arrow. + """ + from matplotlib.bezier import get_cos_sin + + x0, y0 = path.vertices[-2] + x1, y1 = path.vertices[-1] + cost, sint = get_cos_sin(x0, y0, x1, y1) + + d = mutation_size * 1. + x2, y2 = x1 + cost*d, y1+sint*d + + if path.codes is None: + _path = Path(np.concatenate([path.vertices, [[x2, y2]]])) + else: + _path = Path(np.concatenate([path.vertices, [[x2, y2]]]), + np.concatenate([path.codes, [Path.LINETO]])) + + return _path + + def set_path(self, path): + self._line_path = path + + def draw(self, renderer): + """ + Draw the axis line. + 1) transform the path to the display coordinate. + 2) extend the path to make a room for arrow + 3) update the path of the FancyArrowPatch. + 4) draw + """ + path_in_disp = self._line_transform.transform_path(self._line_path) + mutation_size = self.get_mutation_scale() #line_mutation_scale() + extented_path = self._extend_path(path_in_disp, + mutation_size=mutation_size) + + self._path_original = extented_path + FancyArrowPatch.draw(self, renderer) + + class FilledArrow(SimpleArrow): + """ + The artist class that will be returned for SimpleArrow style. + """ + _ARROW_STYLE = "-|>" + + +class AxislineStyle(_Style): + """ + :class:`AxislineStyle` is a container class which defines style classes + for AxisArtists. + + An instance of any axisline style class is an callable object, + whose call signature is :: + + __call__(self, axis_artist, path, transform) + + When called, this should return a mpl artist with following + methods implemented. :: + + def set_path(self, path): + # set the path for axisline. + + def set_line_mutation_scale(self, scale): + # set the scale + + def draw(self, renderer): + # draw + + + """ + + _style_list = {} + + + class _Base(object): + # The derived classes are required to be able to be initialized + # w/o arguments, i.e., all its argument (except self) must have + # the default values. + + def __init__(self): + """ + initialization. + """ + super(AxislineStyle._Base, self).__init__() + + + + + def __call__(self, axis_artist, transform): + """ + Given the AxisArtist instance, and transform for the path + (set_path method), return the mpl artist for drawing the axis line. + """ + + return self.new_line(axis_artist, transform) + + + class SimpleArrow(_Base): + """ + A simple arrow. + """ + + ArrowAxisClass = _FancyAxislineStyle.SimpleArrow + + def __init__(self, size=1): + """ + *size* + size of the arrow as a fraction of the ticklabel size. + """ + + self.size = size + super(AxislineStyle.SimpleArrow, self).__init__() + + def new_line(self, axis_artist, transform): + + linepath = Path([(0,0), (0, 1)]) + axisline = self.ArrowAxisClass(axis_artist, linepath, transform, + line_mutation_scale=self.size) + return axisline + + + _style_list["->"] = SimpleArrow + + class FilledArrow(SimpleArrow): + ArrowAxisClass = _FancyAxislineStyle.FilledArrow + + _style_list["-|>"] = FilledArrow diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/axislines.py b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/axislines.py new file mode 100644 index 0000000000..6182608cc5 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/axislines.py @@ -0,0 +1,828 @@ +""" +Axislines includes modified implementation of the Axes class. The +biggest difference is that the artists responsible for drawing the axis spine, +ticks, ticklabels and axis labels are separated out from mpl's Axis +class. Originally, this change was motivated to support curvilinear +grid. Here are a few reasons that I came up with a new axes class: + + + * "top" and "bottom" x-axis (or "left" and "right" y-axis) can have + different ticks (tick locations and labels). This is not possible + with the current mpl, although some twin axes trick can help. + + * Curvilinear grid. + + * angled ticks. + +In the new axes class, xaxis and yaxis is set to not visible by +default, and new set of artist (AxisArtist) are defined to draw axis +line, ticks, ticklabels and axis label. Axes.axis attribute serves as +a dictionary of these artists, i.e., ax.axis["left"] is a AxisArtist +instance responsible to draw left y-axis. The default Axes.axis contains +"bottom", "left", "top" and "right". + +AxisArtist can be considered as a container artist and +has following children artists which will draw ticks, labels, etc. + + * line + * major_ticks, major_ticklabels + * minor_ticks, minor_ticklabels + * offsetText + * label + +Note that these are separate artists from Axis class of the +original mpl, thus most of tick-related command in the original mpl +won't work, although some effort has made to work with. For example, +color and markerwidth of the ax.axis["bottom"].major_ticks will follow +those of Axes.xaxis unless explicitly specified. + +In addition to AxisArtist, the Axes will have *gridlines* attribute, +which obviously draws grid lines. The gridlines needs to be separated +from the axis as some gridlines can never pass any axis. + +""" +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six + +import warnings + +import numpy as np + +from matplotlib import rcParams +import matplotlib.artist as martist +import matplotlib.axes as maxes +from matplotlib.path import Path +from matplotlib.transforms import Bbox +from .axisline_style import AxislineStyle +from .axis_artist import AxisArtist, GridlinesCollection + + +class AxisArtistHelper(object): + """ + AxisArtistHelper should define + following method with given APIs. Note that the first axes argument + will be axes attribute of the caller artist.:: + + + # LINE (spinal line?) + + def get_line(self, axes): + # path : Path + return path + + def get_line_transform(self, axes): + # ... + # trans : transform + return trans + + # LABEL + + def get_label_pos(self, axes): + # x, y : position + return (x, y), trans + + + def get_label_offset_transform(self, \ + axes, + pad_points, fontprops, renderer, + bboxes, + ): + # va : vertical alignment + # ha : horizontal alignment + # a : angle + return trans, va, ha, a + + # TICK + + def get_tick_transform(self, axes): + return trans + + def get_tick_iterators(self, axes): + # iter : iterable object that yields (c, angle, l) where + # c, angle, l is position, tick angle, and label + + return iter_major, iter_minor + + + """ + + class _Base(object): + """ + Base class for axis helper. + """ + def __init__(self): + """ + """ + self.delta1, self.delta2 = 0.00001, 0.00001 + + def update_lim(self, axes): + pass + + + class Fixed(_Base): + """ + Helper class for a fixed (in the axes coordinate) axis. + """ + + _default_passthru_pt = dict(left=(0, 0), + right=(1, 0), + bottom=(0, 0), + top=(0, 1)) + + def __init__(self, + loc, nth_coord=None, + ): + """ + nth_coord = along which coordinate value varies + in 2d, nth_coord = 0 -> x axis, nth_coord = 1 -> y axis + """ + + self._loc = loc + + if loc not in ["left", "right", "bottom", "top"]: + raise ValueError("%s" % loc) + + if nth_coord is None: + if loc in ["left", "right"]: + nth_coord = 1 + elif loc in ["bottom", "top"]: + nth_coord = 0 + + self.nth_coord = nth_coord + + super(AxisArtistHelper.Fixed, self).__init__() + + self.passthru_pt = self._default_passthru_pt[loc] + + + + _verts = np.array([[0., 0.], + [1., 1.]]) + fixed_coord = 1-nth_coord + _verts[:,fixed_coord] = self.passthru_pt[fixed_coord] + + # axis line in transAxes + self._path = Path(_verts) + + + def get_nth_coord(self): + return self.nth_coord + + # LINE + + def get_line(self, axes): + return self._path + + def get_line_transform(self, axes): + return axes.transAxes + + # LABEL + + def get_axislabel_transform(self, axes): + return axes.transAxes + + def get_axislabel_pos_angle(self, axes): + """ + label reference position in transAxes. + + get_label_transform() returns a transform of (transAxes+offset) + """ + loc = self._loc + pos, angle_tangent = dict(left=((0., 0.5), 90), + right=((1., 0.5), 90), + bottom=((0.5, 0.), 0), + top=((0.5, 1.), 0))[loc] + + return pos, angle_tangent + + + + # TICK + + def get_tick_transform(self, axes): + trans_tick = [axes.get_xaxis_transform(), + axes.get_yaxis_transform()][self.nth_coord] + + return trans_tick + + + class Floating(_Base): + def __init__(self, nth_coord, + value): + + self.nth_coord = nth_coord + + self._value = value + + super(AxisArtistHelper.Floating, + self).__init__() + + + def get_nth_coord(self): + return self.nth_coord + + def get_line(self, axes): + raise RuntimeError("get_line method should be defined by the derived class") + + + + +class AxisArtistHelperRectlinear(object): + + class Fixed(AxisArtistHelper.Fixed): + + def __init__(self, axes, loc, nth_coord=None): + """ + nth_coord = along which coordinate value varies + in 2d, nth_coord = 0 -> x axis, nth_coord = 1 -> y axis + """ + super(AxisArtistHelperRectlinear.Fixed, self).__init__( + loc, nth_coord) + self.axis = [axes.xaxis, axes.yaxis][self.nth_coord] + + # TICK + + def get_tick_iterators(self, axes): + """tick_loc, tick_angle, tick_label""" + + loc = self._loc + + if loc in ["bottom", "top"]: + angle_normal, angle_tangent = 90, 0 + else: + angle_normal, angle_tangent = 0, 90 + + major = self.axis.major + majorLocs = major.locator() + major.formatter.set_locs(majorLocs) + majorLabels = [major.formatter(val, i) for i, val in enumerate(majorLocs)] + + minor = self.axis.minor + minorLocs = minor.locator() + minor.formatter.set_locs(minorLocs) + minorLabels = [minor.formatter(val, i) for i, val in enumerate(minorLocs)] + + trans_tick = self.get_tick_transform(axes) + + tr2ax = trans_tick + axes.transAxes.inverted() + + def _f(locs, labels): + for x, l in zip(locs, labels): + + c = list(self.passthru_pt) # copy + c[self.nth_coord] = x + + # check if the tick point is inside axes + c2 = tr2ax.transform_point(c) + #delta=0.00001 + if 0. -self.delta1<= c2[self.nth_coord] <= 1.+self.delta2: + yield c, angle_normal, angle_tangent, l + + return _f(majorLocs, majorLabels), _f(minorLocs, minorLabels) + + + + class Floating(AxisArtistHelper.Floating): + def __init__(self, axes, nth_coord, + passingthrough_point, axis_direction="bottom"): + super(AxisArtistHelperRectlinear.Floating, self).__init__( + nth_coord, passingthrough_point) + self._axis_direction = axis_direction + self.axis = [axes.xaxis, axes.yaxis][self.nth_coord] + + def get_line(self, axes): + _verts = np.array([[0., 0.], + [1., 1.]]) + + fixed_coord = 1-self.nth_coord + trans_passingthrough_point = axes.transData + axes.transAxes.inverted() + p = trans_passingthrough_point.transform_point([self._value, + self._value]) + _verts[:,fixed_coord] = p[fixed_coord] + + return Path(_verts) + + def get_line_transform(self, axes): + return axes.transAxes + + def get_axislabel_transform(self, axes): + return axes.transAxes + + def get_axislabel_pos_angle(self, axes): + """ + label reference position in transAxes. + + get_label_transform() returns a transform of (transAxes+offset) + """ + loc = self._axis_direction + #angle = dict(left=0, + # right=0, + # bottom=.5*np.pi, + # top=.5*np.pi)[loc] + + if self.nth_coord == 0: + angle = 0 + else: + angle = 90 + + _verts = [0.5, 0.5] + + fixed_coord = 1-self.nth_coord + trans_passingthrough_point = axes.transData + axes.transAxes.inverted() + p = trans_passingthrough_point.transform_point([self._value, + self._value]) + _verts[fixed_coord] = p[fixed_coord] + if not (0. <= _verts[fixed_coord] <= 1.): + return None, None + else: + return _verts, angle + + + + def get_tick_transform(self, axes): + return axes.transData + + + def get_tick_iterators(self, axes): + """tick_loc, tick_angle, tick_label""" + + loc = self._axis_direction + + if loc in ["bottom", "top"]: + angle_normal, angle_tangent = 90, 0 + else: + angle_normal, angle_tangent = 0, 90 + + if self.nth_coord == 0: + angle_normal, angle_tangent = 90, 0 + else: + angle_normal, angle_tangent = 0, 90 + + #angle = 90 - 90 * self.nth_coord + + major = self.axis.major + majorLocs = major.locator() + major.formatter.set_locs(majorLocs) + majorLabels = [major.formatter(val, i) for i, val in enumerate(majorLocs)] + + minor = self.axis.minor + minorLocs = minor.locator() + minor.formatter.set_locs(minorLocs) + minorLabels = [minor.formatter(val, i) for i, val in enumerate(minorLocs)] + + tr2ax = axes.transData + axes.transAxes.inverted() + + def _f(locs, labels): + for x, l in zip(locs, labels): + + c = [self._value, self._value] + c[self.nth_coord] = x + c1, c2 = tr2ax.transform_point(c) + if 0. <= c1 <= 1. and 0. <= c2 <= 1.: + if 0. - self.delta1 <= [c1, c2][self.nth_coord] <= 1. + self.delta2: + yield c, angle_normal, angle_tangent, l + + return _f(majorLocs, majorLabels), _f(minorLocs, minorLabels) + + + + + +class GridHelperBase(object): + + def __init__(self): + self._force_update = True + self._old_limits = None + super(GridHelperBase, self).__init__() + + + def update_lim(self, axes): + x1, x2 = axes.get_xlim() + y1, y2 = axes.get_ylim() + + if self._force_update or self._old_limits != (x1, x2, y1, y2): + self._update(x1, x2, y1, y2) + self._force_update = False + self._old_limits = (x1, x2, y1, y2) + + + def _update(self, x1, x2, y1, y2): + pass + + + def invalidate(self): + self._force_update = True + + def valid(self): + return not self._force_update + + + def get_gridlines(self, which, axis): + """ + Return list of grid lines as a list of paths (list of points). + + *which* : "major" or "minor" + *axis* : "both", "x" or "y" + """ + return [] + + def new_gridlines(self, ax): + """ + Create and return a new GridlineCollection instance. + + *which* : "major" or "minor" + *axis* : "both", "x" or "y" + + """ + gridlines = GridlinesCollection(None, transform=ax.transData, + colors=rcParams['grid.color'], + linestyles=rcParams['grid.linestyle'], + linewidths=rcParams['grid.linewidth']) + ax._set_artist_props(gridlines) + gridlines.set_grid_helper(self) + + ax.axes._set_artist_props(gridlines) + # gridlines.set_clip_path(self.axes.patch) + # set_clip_path need to be deferred after Axes.cla is completed. + # It is done inside the cla. + + return gridlines + + +class GridHelperRectlinear(GridHelperBase): + + + def __init__(self, axes): + + super(GridHelperRectlinear, self).__init__() + self.axes = axes + + + + def new_fixed_axis(self, loc, + nth_coord=None, + axis_direction=None, + offset=None, + axes=None, + ): + + if axes is None: + warnings.warn("'new_fixed_axis' explicitly requires the axes keyword.") + axes = self.axes + + _helper = AxisArtistHelperRectlinear.Fixed(axes, loc, nth_coord) + + if axis_direction is None: + axis_direction = loc + axisline = AxisArtist(axes, _helper, offset=offset, + axis_direction=axis_direction, + ) + + return axisline + + + def new_floating_axis(self, nth_coord, value, + axis_direction="bottom", + axes=None, + ): + + if axes is None: + warnings.warn( + "'new_floating_axis' explicitly requires the axes keyword.") + axes = self.axes + + passthrough_point = (value, value) + transform = axes.transData + + _helper = AxisArtistHelperRectlinear.Floating( + axes, nth_coord, value, axis_direction) + + axisline = AxisArtist(axes, _helper) + + axisline.line.set_clip_on(True) + axisline.line.set_clip_box(axisline.axes.bbox) + return axisline + + + def get_gridlines(self, which="major", axis="both"): + """ + return list of gridline coordinates in data coordinates. + + *which* : "major" or "minor" + *axis* : "both", "x" or "y" + """ + + gridlines = [] + + + if axis in ["both", "x"]: + locs = [] + y1, y2 = self.axes.get_ylim() + #if self.axes.xaxis._gridOnMajor: + if which in ["both", "major"]: + locs.extend(self.axes.xaxis.major.locator()) + #if self.axes.xaxis._gridOnMinor: + if which in ["both", "minor"]: + locs.extend(self.axes.xaxis.minor.locator()) + + for x in locs: + gridlines.append([[x, x], [y1, y2]]) + + + if axis in ["both", "y"]: + x1, x2 = self.axes.get_xlim() + locs = [] + if self.axes.yaxis._gridOnMajor: + #if which in ["both", "major"]: + locs.extend(self.axes.yaxis.major.locator()) + if self.axes.yaxis._gridOnMinor: + #if which in ["both", "minor"]: + locs.extend(self.axes.yaxis.minor.locator()) + + for y in locs: + gridlines.append([[x1, x2], [y, y]]) + + return gridlines + + + + + + +class SimpleChainedObjects(object): + def __init__(self, objects): + self._objects = objects + + def __getattr__(self, k): + _a = SimpleChainedObjects([getattr(a, k) for a in self._objects]) + return _a + + def __call__(self, *kl, **kwargs): + for m in self._objects: + m(*kl, **kwargs) + + +class Axes(maxes.Axes): + + class AxisDict(dict): + def __init__(self, axes): + self.axes = axes + super(Axes.AxisDict, self).__init__() + + def __getitem__(self, k): + if isinstance(k, tuple): + r = SimpleChainedObjects([dict.__getitem__(self, k1) for k1 in k]) + return r + elif isinstance(k, slice): + if k.start == None and k.stop == None and k.step == None: + r = SimpleChainedObjects(list(six.itervalues(self))) + return r + else: + raise ValueError("Unsupported slice") + else: + return dict.__getitem__(self, k) + + def __call__(self, *v, **kwargs): + return maxes.Axes.axis(self.axes, *v, **kwargs) + + + def __init__(self, *kl, **kw): + + + helper = kw.pop("grid_helper", None) + + self._axisline_on = True + + if helper: + self._grid_helper = helper + else: + self._grid_helper = GridHelperRectlinear(self) + + super(Axes, self).__init__(*kl, **kw) + + self.toggle_axisline(True) + + + def toggle_axisline(self, b=None): + if b is None: + b = not self._axisline_on + if b: + self._axisline_on = True + for s in self.spines.values(): + s.set_visible(False) + self.xaxis.set_visible(False) + self.yaxis.set_visible(False) + else: + self._axisline_on = False + for s in self.spines.values(): + s.set_visible(True) + self.xaxis.set_visible(True) + self.yaxis.set_visible(True) + + + def _init_axis(self): + super(Axes, self)._init_axis() + + + def _init_axis_artists(self, axes=None): + if axes is None: + axes = self + + self._axislines = self.AxisDict(self) + new_fixed_axis = self.get_grid_helper().new_fixed_axis + for loc in ["bottom", "top", "left", "right"]: + self._axislines[loc] = new_fixed_axis(loc=loc, axes=axes, + axis_direction=loc) + + for axisline in [self._axislines["top"], self._axislines["right"]]: + axisline.label.set_visible(False) + axisline.major_ticklabels.set_visible(False) + axisline.minor_ticklabels.set_visible(False) + + @property + def axis(self): + return self._axislines + + def new_gridlines(self, grid_helper=None): + """ + Create and return a new GridlineCollection instance. + + *which* : "major" or "minor" + *axis* : "both", "x" or "y" + + """ + if grid_helper is None: + grid_helper = self.get_grid_helper() + + gridlines = grid_helper.new_gridlines(self) + + return gridlines + + + def _init_gridlines(self, grid_helper=None): + # It is done inside the cla. + gridlines = self.new_gridlines(grid_helper) + + self.gridlines = gridlines + + def cla(self): + # gridlines need to b created before cla() since cla calls grid() + + self._init_gridlines() + super(Axes, self).cla() + + # the clip_path should be set after Axes.cla() since that's + # when a patch is created. + self.gridlines.set_clip_path(self.axes.patch) + + self._init_axis_artists() + + def get_grid_helper(self): + return self._grid_helper + + + def grid(self, b=None, which='major', axis="both", **kwargs): + """ + Toggle the gridlines, and optionally set the properties of the lines. + """ + # their are some discrepancy between the behavior of grid in + # axes_grid and the original mpl's grid, because axes_grid + # explicitly set the visibility of the gridlines. + + super(Axes, self).grid(b, which=which, axis=axis, **kwargs) + if not self._axisline_on: + return + + if b is None: + + if self.axes.xaxis._gridOnMinor or self.axes.xaxis._gridOnMajor or \ + self.axes.yaxis._gridOnMinor or self.axes.yaxis._gridOnMajor: + b=True + else: + b=False + + self.gridlines.set_which(which) + self.gridlines.set_axis(axis) + self.gridlines.set_visible(b) + + if len(kwargs): + martist.setp(self.gridlines, **kwargs) + + def get_children(self): + if self._axisline_on: + children = list(six.itervalues(self._axislines)) + [self.gridlines] + else: + children = [] + children.extend(super(Axes, self).get_children()) + return children + + def invalidate_grid_helper(self): + self._grid_helper.invalidate() + + + def new_fixed_axis(self, loc, offset=None): + gh = self.get_grid_helper() + axis = gh.new_fixed_axis(loc, + nth_coord=None, + axis_direction=None, + offset=offset, + axes=self, + ) + return axis + + + def new_floating_axis(self, nth_coord, value, + axis_direction="bottom", + ): + gh = self.get_grid_helper() + axis = gh.new_floating_axis(nth_coord, value, + axis_direction=axis_direction, + axes=self) + return axis + + + + def draw(self, renderer, inframe=False): + + if not self._axisline_on: + super(Axes, self).draw(renderer, inframe) + return + + orig_artists = self.artists + self.artists = self.artists + list(self._axislines.values()) + [self.gridlines] + + super(Axes, self).draw(renderer, inframe) + + self.artists = orig_artists + + + def get_tightbbox(self, renderer, call_axes_locator=True): + + bb0 = super(Axes, self).get_tightbbox(renderer, call_axes_locator) + + if not self._axisline_on: + return bb0 + + bb = [bb0] + + for axisline in list(six.itervalues(self._axislines)): + if not axisline.get_visible(): + continue + + bb.append(axisline.get_tightbbox(renderer)) + # if axisline.label.get_visible(): + # bb.append(axisline.label.get_window_extent(renderer)) + + + # if axisline.major_ticklabels.get_visible(): + # bb.extend(axisline.major_ticklabels.get_window_extents(renderer)) + # if axisline.minor_ticklabels.get_visible(): + # bb.extend(axisline.minor_ticklabels.get_window_extents(renderer)) + # if axisline.major_ticklabels.get_visible() or \ + # axisline.minor_ticklabels.get_visible(): + # bb.append(axisline.offsetText.get_window_extent(renderer)) + + #bb.extend([c.get_window_extent(renderer) for c in artists \ + # if c.get_visible()]) + + _bbox = Bbox.union([b for b in bb if b and (b.width!=0 or b.height!=0)]) + + return _bbox + + + + +Subplot = maxes.subplot_class_factory(Axes) + +class AxesZero(Axes): + def __init__(self, *kl, **kw): + + super(AxesZero, self).__init__(*kl, **kw) + + + def _init_axis_artists(self): + super(AxesZero, self)._init_axis_artists() + + new_floating_axis = self._grid_helper.new_floating_axis + xaxis_zero = new_floating_axis(nth_coord=0, + value=0., + axis_direction="bottom", + axes=self) + + xaxis_zero.line.set_clip_path(self.patch) + xaxis_zero.set_visible(False) + self._axislines["xzero"] = xaxis_zero + + yaxis_zero = new_floating_axis(nth_coord=1, + value=0., + axis_direction="left", + axes=self) + + + yaxis_zero.line.set_clip_path(self.patch) + yaxis_zero.set_visible(False) + self._axislines["yzero"] = yaxis_zero + +SubplotZero = maxes.subplot_class_factory(AxesZero) diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/clip_path.py b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/clip_path.py new file mode 100644 index 0000000000..8507b09b07 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/clip_path.py @@ -0,0 +1,135 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six +from six.moves import zip + +import numpy as np +from math import degrees +import math +import warnings + +def atan2(dy, dx): + if dx == 0 and dy == 0: + warnings.warn("dx and dy is 0") + return 0 + else: + return math.atan2(dy, dx) + +# FIXME : The current algorithm seems to return incorrect angle when the line +# ends at the boundary. + +def clip(xlines, ylines, x0, clip="right", xdir=True, ydir=True): + + clipped_xlines = [] + clipped_ylines = [] + + _pos_angles = [] + + if xdir: + xsign = 1 + else: + xsign = -1 + + if ydir: + ysign = 1 + else: + ysign = -1 + + + for x, y in zip(xlines, ylines): + + if clip in ["up", "right"]: + b = (x < x0).astype("i") + db = b[1:] - b[:-1] + else: + b = (x > x0).astype("i") + db = b[1:] - b[:-1] + + + if b[0]: + ns = 0 + else: + ns = -1 + segx, segy = [], [] + for (i,) in np.argwhere(db!=0): + c = db[i] + if c == -1: + dx = (x0 - x[i]) + dy = (y[i+1] - y[i]) * (dx/ (x[i+1] - x[i])) + y0 = y[i] + dy + clipped_xlines.append(np.concatenate([segx, x[ns:i+1], [x0]])) + clipped_ylines.append(np.concatenate([segy, y[ns:i+1], [y0]])) + ns = -1 + segx, segy = [], [] + + if dx == 0. and dy == 0: + dx = x[i+1] - x[i] + dy = y[i+1] - y[i] + + a = degrees(atan2(ysign*dy, xsign*dx)) + _pos_angles.append((x0, y0, a)) + + elif c == 1: + dx = (x0 - x[i]) + dy = (y[i+1] - y[i]) * (dx / (x[i+1] - x[i])) + y0 = y[i] + dy + segx, segy = [x0], [y0] + ns = i+1 + + if dx == 0. and dy == 0: + dx = x[i+1] - x[i] + dy = y[i+1] - y[i] + + a = degrees(atan2(ysign*dy, xsign*dx)) + _pos_angles.append((x0, y0, a)) + + if ns != -1: + clipped_xlines.append(np.concatenate([segx, x[ns:]])) + clipped_ylines.append(np.concatenate([segy, y[ns:]])) + + #clipped_pos_angles.append(_pos_angles) + + + return clipped_xlines, clipped_ylines, _pos_angles + + +def clip_line_to_rect(xline, yline, bbox): + + x0, y0, x1, y1 = bbox.extents + + xdir = x1 > x0 + ydir = y1 > y0 + + if x1 > x0: + lx1, ly1, c_right_ = clip([xline], [yline], x1, clip="right", xdir=xdir, ydir=ydir) + lx2, ly2, c_left_ = clip(lx1, ly1, x0, clip="left", xdir=xdir, ydir=ydir) + else: + lx1, ly1, c_right_ = clip([xline], [yline], x0, clip="right", xdir=xdir, ydir=ydir) + lx2, ly2, c_left_ = clip(lx1, ly1, x1, clip="left", xdir=xdir, ydir=ydir) + + if y1 > y0: + ly3, lx3, c_top_ = clip(ly2, lx2, y1, clip="right", xdir=ydir, ydir=xdir) + ly4, lx4, c_bottom_ = clip(ly3, lx3, y0, clip="left", xdir=ydir, ydir=xdir) + else: + ly3, lx3, c_top_ = clip(ly2, lx2, y0, clip="right", xdir=ydir, ydir=xdir) + ly4, lx4, c_bottom_ = clip(ly3, lx3, y1, clip="left", xdir=ydir, ydir=xdir) + + + # lx1, ly1, c_right_ = clip([xline], [yline], x1, clip="right") + # lx2, ly2, c_left_ = clip(lx1, ly1, x0, clip="left") + # ly3, lx3, c_top_ = clip(ly2, lx2, y1, clip="right") + # ly4, lx4, c_bottom_ = clip(ly3, lx3, y0, clip="left") + + #c_left = [((x, y), (a+90)%180-180) for (x, y, a) in c_left_ \ + # if bbox.containsy(y)] + c_left = [((x, y), (a+90)%180-90) for (x, y, a) in c_left_ + if bbox.containsy(y)] + c_bottom = [((x, y), (90 - a)%180) for (y, x, a) in c_bottom_ + if bbox.containsx(x)] + c_right = [((x, y), (a+90)%180+90) for (x, y, a) in c_right_ + if bbox.containsy(y)] + c_top = [((x, y), (90 - a)%180+180) for (y, x, a) in c_top_ + if bbox.containsx(x)] + + return list(zip(lx4, ly4)), [c_left, c_bottom, c_right, c_top] diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/floating_axes.py b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/floating_axes.py new file mode 100644 index 0000000000..468413dbac --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/floating_axes.py @@ -0,0 +1,544 @@ +""" +An experimental support for curvilinear grid. +""" +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six +from six.moves import zip + +# TODO : +# see if tick_iterator method can be simplified by reusing the parent method. + +import numpy as np + +from matplotlib.transforms import Affine2D, IdentityTransform +from . import grid_helper_curvelinear +from .axislines import AxisArtistHelper, GridHelperBase +from .axis_artist import AxisArtist +from .grid_finder import GridFinder + + +class FloatingAxisArtistHelper(grid_helper_curvelinear.FloatingAxisArtistHelper): + pass + + +class FixedAxisArtistHelper(grid_helper_curvelinear.FloatingAxisArtistHelper): + + def __init__(self, grid_helper, side, nth_coord_ticks=None): + """ + nth_coord = along which coordinate value varies. + nth_coord = 0 -> x axis, nth_coord = 1 -> y axis + """ + + value, nth_coord = grid_helper.get_data_boundary(side) # return v= 0 , nth=1, extremes of the other coordinate. + super(FixedAxisArtistHelper, self).__init__(grid_helper, + nth_coord, + value, + axis_direction=side, + ) + #self.grid_helper = grid_helper + if nth_coord_ticks is None: + nth_coord_ticks = nth_coord + self.nth_coord_ticks = nth_coord_ticks + + self.value = value + self.grid_helper = grid_helper + self._side = side + + + def update_lim(self, axes): + self.grid_helper.update_lim(axes) + + self.grid_info = self.grid_helper.grid_info + + + + def get_axislabel_pos_angle(self, axes): + + extremes = self.grid_info["extremes"] + + if self.nth_coord == 0: + xx0 = self.value + yy0 = (extremes[2]+extremes[3])/2. + dxx, dyy = 0., abs(extremes[2]-extremes[3])/1000. + elif self.nth_coord == 1: + xx0 = (extremes[0]+extremes[1])/2. + yy0 = self.value + dxx, dyy = abs(extremes[0]-extremes[1])/1000., 0. + + grid_finder = self.grid_helper.grid_finder + xx1, yy1 = grid_finder.transform_xy([xx0], [yy0]) + + trans_passingthrough_point = axes.transData + axes.transAxes.inverted() + p = trans_passingthrough_point.transform_point([xx1[0], yy1[0]]) + + + if (0. <= p[0] <= 1.) and (0. <= p[1] <= 1.): + xx1c, yy1c = axes.transData.transform_point([xx1[0], yy1[0]]) + xx2, yy2 = grid_finder.transform_xy([xx0+dxx], [yy0+dyy]) + xx2c, yy2c = axes.transData.transform_point([xx2[0], yy2[0]]) + + return (xx1c, yy1c), np.arctan2(yy2c-yy1c, xx2c-xx1c)/np.pi*180. + else: + return None, None + + + + def get_tick_transform(self, axes): + return IdentityTransform() #axes.transData + + def get_tick_iterators(self, axes): + """tick_loc, tick_angle, tick_label, (optionally) tick_label""" + + + grid_finder = self.grid_helper.grid_finder + + lat_levs, lat_n, lat_factor = self.grid_info["lat_info"] + lon_levs, lon_n, lon_factor = self.grid_info["lon_info"] + + lon_levs, lat_levs = np.asarray(lon_levs), np.asarray(lat_levs) + if lat_factor is not None: + yy0 = lat_levs / lat_factor + dy = 0.001 / lat_factor + else: + yy0 = lat_levs + dy = 0.001 + + if lon_factor is not None: + xx0 = lon_levs / lon_factor + dx = 0.001 / lon_factor + else: + xx0 = lon_levs + dx = 0.001 + + _extremes = self.grid_helper._extremes + xmin, xmax = sorted(_extremes[:2]) + ymin, ymax = sorted(_extremes[2:]) + if self.nth_coord == 0: + mask = (ymin <= yy0) & (yy0 <= ymax) + yy0 = yy0[mask] + elif self.nth_coord == 1: + mask = (xmin <= xx0) & (xx0 <= xmax) + xx0 = xx0[mask] + + def transform_xy(x, y): + x1, y1 = grid_finder.transform_xy(x, y) + x2y2 = axes.transData.transform(np.array([x1, y1]).transpose()) + x2, y2 = x2y2.transpose() + return x2, y2 + + # find angles + if self.nth_coord == 0: + xx0 = np.empty_like(yy0) + xx0.fill(self.value) + + #yy0_ = yy0.copy() + + xx1, yy1 = transform_xy(xx0, yy0) + + xx00 = xx0.astype(float, copy=True) + xx00[xx0+dx>xmax] -= dx + xx1a, yy1a = transform_xy(xx00, yy0) + xx1b, yy1b = transform_xy(xx00+dx, yy0) + + yy00 = yy0.astype(float, copy=True) + yy00[yy0+dy>ymax] -= dy + xx2a, yy2a = transform_xy(xx0, yy00) + xx2b, yy2b = transform_xy(xx0, yy00+dy) + + labels = self.grid_info["lat_labels"] + labels = [l for l, m in zip(labels, mask) if m] + + elif self.nth_coord == 1: + yy0 = np.empty_like(xx0) + yy0.fill(self.value) + + #xx0_ = xx0.copy() + xx1, yy1 = transform_xy(xx0, yy0) + + + yy00 = yy0.astype(float, copy=True) + yy00[yy0+dy>ymax] -= dy + xx1a, yy1a = transform_xy(xx0, yy00) + xx1b, yy1b = transform_xy(xx0, yy00+dy) + + xx00 = xx0.astype(float, copy=True) + xx00[xx0+dx>xmax] -= dx + xx2a, yy2a = transform_xy(xx00, yy0) + xx2b, yy2b = transform_xy(xx00+dx, yy0) + + labels = self.grid_info["lon_labels"] + labels = [l for l, m in zip(labels, mask) if m] + + + def f1(): + dd = np.arctan2(yy1b-yy1a, xx1b-xx1a) # angle normal + dd2 = np.arctan2(yy2b-yy2a, xx2b-xx2a) # angle tangent + mm = ((yy1b-yy1a)==0.) & ((xx1b-xx1a)==0.) # mask where dd1 is not defined + dd[mm] = dd2[mm] + np.pi / 2 + + #dd += np.pi + #dd = np.arctan2(xx2-xx1, angle_tangent-yy1) + trans_tick = self.get_tick_transform(axes) + tr2ax = trans_tick + axes.transAxes.inverted() + for x, y, d, d2, lab in zip(xx1, yy1, dd, dd2, labels): + c2 = tr2ax.transform_point((x, y)) + delta=0.00001 + if (0. -delta<= c2[0] <= 1.+delta) and \ + (0. -delta<= c2[1] <= 1.+delta): + d1 = d/3.14159*180. + d2 = d2/3.14159*180. + #_mod = (d2-d1+180)%360 + #if _mod < 180: + # d1 += 180 + ##_div, _mod = divmod(d2-d1, 360) + yield [x, y], d1, d2, lab + #, d2/3.14159*180.+da) + + return f1(), iter([]) + + def get_line_transform(self, axes): + return axes.transData + + def get_line(self, axes): + + self.update_lim(axes) + from matplotlib.path import Path + k, v = dict(left=("lon_lines0", 0), + right=("lon_lines0", 1), + bottom=("lat_lines0", 0), + top=("lat_lines0", 1))[self._side] + + xx, yy = self.grid_info[k][v] + return Path(np.column_stack([xx, yy])) + + + +from .grid_finder import ExtremeFinderSimple + +class ExtremeFinderFixed(ExtremeFinderSimple): + def __init__(self, extremes): + self._extremes = extremes + + def __call__(self, transform_xy, x1, y1, x2, y2): + """ + get extreme values. + + x1, y1, x2, y2 in image coordinates (0-based) + nx, ny : number of division in each axis + """ + #lon_min, lon_max, lat_min, lat_max = self._extremes + return self._extremes + + + +class GridHelperCurveLinear(grid_helper_curvelinear.GridHelperCurveLinear): + + def __init__(self, aux_trans, extremes, + grid_locator1=None, + grid_locator2=None, + tick_formatter1=None, + tick_formatter2=None): + """ + aux_trans : a transform from the source (curved) coordinate to + target (rectilinear) coordinate. An instance of MPL's Transform + (inverse transform should be defined) or a tuple of two callable + objects which defines the transform and its inverse. The callables + need take two arguments of array of source coordinates and + should return two target coordinates: + e.g., *x2, y2 = trans(x1, y1)* + """ + + self._old_values = None + + self._extremes = extremes + extreme_finder = ExtremeFinderFixed(extremes) + + super(GridHelperCurveLinear, self).__init__(aux_trans, + extreme_finder, + grid_locator1=grid_locator1, + grid_locator2=grid_locator2, + tick_formatter1=tick_formatter1, + tick_formatter2=tick_formatter2) + + + # def update_grid_finder(self, aux_trans=None, **kw): + + # if aux_trans is not None: + # self.grid_finder.update_transform(aux_trans) + + # self.grid_finder.update(**kw) + # self.invalidate() + + + # def _update(self, x1, x2, y1, y2): + # "bbox in 0-based image coordinates" + # # update wcsgrid + + # if self.valid() and self._old_values == (x1, x2, y1, y2): + # return + + # self._update_grid(x1, y1, x2, y2) + + # self._old_values = (x1, x2, y1, y2) + + # self._force_update = False + + + def get_data_boundary(self, side): + """ + return v= 0 , nth=1 + """ + lon1, lon2, lat1, lat2 = self._extremes + return dict(left=(lon1, 0), + right=(lon2, 0), + bottom=(lat1, 1), + top=(lat2, 1))[side] + + + def new_fixed_axis(self, loc, + nth_coord=None, + axis_direction=None, + offset=None, + axes=None): + + if axes is None: + axes = self.axes + + if axis_direction is None: + axis_direction = loc + + _helper = FixedAxisArtistHelper(self, loc, + nth_coord_ticks=nth_coord) + + + axisline = AxisArtist(axes, _helper, axis_direction=axis_direction) + axisline.line.set_clip_on(True) + axisline.line.set_clip_box(axisline.axes.bbox) + + + return axisline + + + # new_floating_axis will inherit the grid_helper's extremes. + + # def new_floating_axis(self, nth_coord, + # value, + # axes=None, + # axis_direction="bottom" + # ): + + # axis = super(GridHelperCurveLinear, + # self).new_floating_axis(nth_coord, + # value, axes=axes, + # axis_direction=axis_direction) + + # # set extreme values of the axis helper + # if nth_coord == 1: + # axis.get_helper().set_extremes(*self._extremes[:2]) + # elif nth_coord == 0: + # axis.get_helper().set_extremes(*self._extremes[2:]) + + # return axis + + + def _update_grid(self, x1, y1, x2, y2): + + #self.grid_info = self.grid_finder.get_grid_info(x1, y1, x2, y2) + + if self.grid_info is None: + self.grid_info = dict() + + grid_info = self.grid_info + + grid_finder = self.grid_finder + extremes = grid_finder.extreme_finder(grid_finder.inv_transform_xy, + x1, y1, x2, y2) + + lon_min, lon_max = sorted(extremes[:2]) + lat_min, lat_max = sorted(extremes[2:]) + lon_levs, lon_n, lon_factor = \ + grid_finder.grid_locator1(lon_min, lon_max) + lat_levs, lat_n, lat_factor = \ + grid_finder.grid_locator2(lat_min, lat_max) + grid_info["extremes"] = lon_min, lon_max, lat_min, lat_max #extremes + + grid_info["lon_info"] = lon_levs, lon_n, lon_factor + grid_info["lat_info"] = lat_levs, lat_n, lat_factor + + grid_info["lon_labels"] = grid_finder.tick_formatter1("bottom", + lon_factor, + lon_levs) + + grid_info["lat_labels"] = grid_finder.tick_formatter2("bottom", + lat_factor, + lat_levs) + + if lon_factor is None: + lon_values = np.asarray(lon_levs[:lon_n]) + else: + lon_values = np.asarray(lon_levs[:lon_n]/lon_factor) + if lat_factor is None: + lat_values = np.asarray(lat_levs[:lat_n]) + else: + lat_values = np.asarray(lat_levs[:lat_n]/lat_factor) + + lon_values0 = lon_values[(lon_min<lon_values) & (lon_values<lon_max)] + lat_values0 = lat_values[(lat_min<lat_values) & (lat_values<lat_max)] + lon_lines, lat_lines = grid_finder._get_raw_grid_lines(lon_values0, + lat_values0, + lon_min, lon_max, + lat_min, lat_max) + + + grid_info["lon_lines"] = lon_lines + grid_info["lat_lines"] = lat_lines + + + lon_lines, lat_lines = grid_finder._get_raw_grid_lines(extremes[:2], + extremes[2:], + *extremes) + #lon_min, lon_max, + # lat_min, lat_max) + + + grid_info["lon_lines0"] = lon_lines + grid_info["lat_lines0"] = lat_lines + + + + def get_gridlines(self, which="major", axis="both"): + grid_lines = [] + if axis in ["both", "x"]: + for gl in self.grid_info["lon_lines"]: + grid_lines.extend([gl]) + if axis in ["both", "y"]: + for gl in self.grid_info["lat_lines"]: + grid_lines.extend([gl]) + + return grid_lines + + + def get_boundary(self): + """ + return Nx2 array of x,y coordinate of the boundary + """ + x0, x1, y0, y1 = self._extremes + tr = self._aux_trans + xx = np.linspace(x0, x1, 100) + yy0, yy1 = np.empty_like(xx), np.empty_like(xx) + yy0.fill(y0) + yy1.fill(y1) + + yy = np.linspace(y0, y1, 100) + xx0, xx1 = np.empty_like(yy), np.empty_like(yy) + xx0.fill(x0) + xx1.fill(x1) + + xxx = np.concatenate([xx[:-1], xx1[:-1], xx[-1:0:-1], xx0]) + yyy = np.concatenate([yy0[:-1], yy[:-1], yy1[:-1], yy[::-1]]) + t = tr.transform(np.array([xxx, yyy]).transpose()) + + return t + + + + + + + + + + + + +class FloatingAxesBase(object): + + + def __init__(self, *kl, **kwargs): + grid_helper = kwargs.get("grid_helper", None) + if grid_helper is None: + raise ValueError("FloatingAxes requires grid_helper argument") + if not hasattr(grid_helper, "get_boundary"): + raise ValueError("grid_helper must implement get_boundary method") + + self._axes_class_floating.__init__(self, *kl, **kwargs) + + self.set_aspect(1.) + self.adjust_axes_lim() + + + def _gen_axes_patch(self): + """ + Returns the patch used to draw the background of the axes. It + is also used as the clipping path for any data elements on the + axes. + + In the standard axes, this is a rectangle, but in other + projections it may not be. + + .. note:: + Intended to be overridden by new projection types. + """ + import matplotlib.patches as mpatches + grid_helper = self.get_grid_helper() + t = grid_helper.get_boundary() + return mpatches.Polygon(t) + + def cla(self): + self._axes_class_floating.cla(self) + #HostAxes.cla(self) + self.patch.set_transform(self.transData) + + + patch = self._axes_class_floating._gen_axes_patch(self) + patch.set_figure(self.figure) + patch.set_visible(False) + patch.set_transform(self.transAxes) + + self.patch.set_clip_path(patch) + self.gridlines.set_clip_path(patch) + + self._original_patch = patch + + + def adjust_axes_lim(self): + + #t = self.get_boundary() + grid_helper = self.get_grid_helper() + t = grid_helper.get_boundary() + x, y = t[:,0], t[:,1] + + xmin, xmax = min(x), max(x) + ymin, ymax = min(y), max(y) + + dx = (xmax-xmin)/100. + dy = (ymax-ymin)/100. + + self.set_xlim(xmin-dx, xmax+dx) + self.set_ylim(ymin-dy, ymax+dy) + + + +_floatingaxes_classes = {} + +def floatingaxes_class_factory(axes_class): + + new_class = _floatingaxes_classes.get(axes_class) + if new_class is None: + new_class = type(str("Floating %s" % (axes_class.__name__)), + (FloatingAxesBase, axes_class), + {'_axes_class_floating': axes_class}) + _floatingaxes_classes[axes_class] = new_class + + return new_class + +from .axislines import Axes +from mpl_toolkits.axes_grid1.parasite_axes import host_axes_class_factory + +FloatingAxes = floatingaxes_class_factory(host_axes_class_factory(Axes)) + + +import matplotlib.axes as maxes +FloatingSubplot = maxes.subplot_class_factory(FloatingAxes) diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/grid_finder.py b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/grid_finder.py new file mode 100644 index 0000000000..62a94b1478 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/grid_finder.py @@ -0,0 +1,340 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six + +import numpy as np +from matplotlib.transforms import Bbox +from . import clip_path +clip_line_to_rect = clip_path.clip_line_to_rect + +import matplotlib.ticker as mticker +from matplotlib.transforms import Transform + +# extremes finder + +class ExtremeFinderSimple(object): + def __init__(self, nx, ny): + self.nx, self.ny = nx, ny + + def __call__(self, transform_xy, x1, y1, x2, y2): + """ + get extreme values. + + x1, y1, x2, y2 in image coordinates (0-based) + nx, ny : number of division in each axis + """ + x_, y_ = np.linspace(x1, x2, self.nx), np.linspace(y1, y2, self.ny) + x, y = np.meshgrid(x_, y_) + lon, lat = transform_xy(np.ravel(x), np.ravel(y)) + + lon_min, lon_max = lon.min(), lon.max() + lat_min, lat_max = lat.min(), lat.max() + + return self._add_pad(lon_min, lon_max, lat_min, lat_max) + + def _add_pad(self, lon_min, lon_max, lat_min, lat_max): + """ a small amount of padding is added because the current + clipping algorithms seems to fail when the gridline ends at + the bbox boundary. + """ + dlon = (lon_max - lon_min) / self.nx + dlat = (lat_max - lat_min) / self.ny + + lon_min, lon_max = lon_min - dlon, lon_max + dlon + lat_min, lat_max = lat_min - dlat, lat_max + dlat + + return lon_min, lon_max, lat_min, lat_max + + + +class GridFinderBase(object): + def __init__(self, + extreme_finder, + grid_locator1, + grid_locator2, + tick_formatter1=None, + tick_formatter2=None): + """ + the transData of the axes to the world coordinate. + locator1, locator2 : grid locator for 1st and 2nd axis. + + Derived must define "transform_xy, inv_transform_xy" + (may use update_transform) + """ + super(GridFinderBase, self).__init__() + + self.extreme_finder = extreme_finder + self.grid_locator1 = grid_locator1 + self.grid_locator2 = grid_locator2 + self.tick_formatter1 = tick_formatter1 + self.tick_formatter2 = tick_formatter2 + + def get_grid_info(self, + x1, y1, x2, y2): + """ + lon_values, lat_values : list of grid values. if integer is given, + rough number of grids in each direction. + """ + + extremes = self.extreme_finder(self.inv_transform_xy, x1, y1, x2, y2) + + # min & max rage of lat (or lon) for each grid line will be drawn. + # i.e., gridline of lon=0 will be drawn from lat_min to lat_max. + + lon_min, lon_max, lat_min, lat_max = extremes + lon_levs, lon_n, lon_factor = \ + self.grid_locator1(lon_min, lon_max) + lat_levs, lat_n, lat_factor = \ + self.grid_locator2(lat_min, lat_max) + + if lon_factor is None: + lon_values = np.asarray(lon_levs[:lon_n]) + else: + lon_values = np.asarray(lon_levs[:lon_n]/lon_factor) + if lat_factor is None: + lat_values = np.asarray(lat_levs[:lat_n]) + else: + lat_values = np.asarray(lat_levs[:lat_n]/lat_factor) + + + lon_lines, lat_lines = self._get_raw_grid_lines(lon_values, + lat_values, + lon_min, lon_max, + lat_min, lat_max) + + ddx = (x2-x1)*1.e-10 + ddy = (y2-y1)*1.e-10 + bb = Bbox.from_extents(x1-ddx, y1-ddy, x2+ddx, y2+ddy) + + grid_info = {} + grid_info["extremes"] = extremes + grid_info["lon_lines"] = lon_lines + grid_info["lat_lines"] = lat_lines + + grid_info["lon"] = self._clip_grid_lines_and_find_ticks(lon_lines, + lon_values, + lon_levs, + bb) + + grid_info["lat"] = self._clip_grid_lines_and_find_ticks(lat_lines, + lat_values, + lat_levs, + bb) + + tck_labels = grid_info["lon"]["tick_labels"] = dict() + for direction in ["left", "bottom", "right", "top"]: + levs = grid_info["lon"]["tick_levels"][direction] + tck_labels[direction] = self.tick_formatter1(direction, + lon_factor, levs) + + tck_labels = grid_info["lat"]["tick_labels"] = dict() + for direction in ["left", "bottom", "right", "top"]: + levs = grid_info["lat"]["tick_levels"][direction] + tck_labels[direction] = self.tick_formatter2(direction, + lat_factor, levs) + + return grid_info + + + def _get_raw_grid_lines(self, + lon_values, lat_values, + lon_min, lon_max, lat_min, lat_max): + + lons_i = np.linspace(lon_min, lon_max, 100) # for interpolation + lats_i = np.linspace(lat_min, lat_max, 100) + + lon_lines = [self.transform_xy(np.zeros_like(lats_i) + lon, lats_i) + for lon in lon_values] + lat_lines = [self.transform_xy(lons_i, np.zeros_like(lons_i) + lat) + for lat in lat_values] + + return lon_lines, lat_lines + + + def _clip_grid_lines_and_find_ticks(self, lines, values, levs, bb): + gi = dict() + gi["values"] = [] + gi["levels"] = [] + gi["tick_levels"] = dict(left=[], bottom=[], right=[], top=[]) + gi["tick_locs"] = dict(left=[], bottom=[], right=[], top=[]) + gi["lines"] = [] + + tck_levels = gi["tick_levels"] + tck_locs = gi["tick_locs"] + for (lx, ly), v, lev in zip(lines, values, levs): + xy, tcks = clip_line_to_rect(lx, ly, bb) + if not xy: + continue + gi["levels"].append(v) + gi["lines"].append(xy) + + for tck, direction in zip(tcks, + ["left", "bottom", "right", "top"]): + for t in tck: + tck_levels[direction].append(lev) + tck_locs[direction].append(t) + + return gi + + + def update_transform(self, aux_trans): + if isinstance(aux_trans, Transform): + def transform_xy(x, y): + x, y = np.asarray(x), np.asarray(y) + ll1 = np.concatenate((x[:,np.newaxis], y[:,np.newaxis]), 1) + ll2 = aux_trans.transform(ll1) + lon, lat = ll2[:,0], ll2[:,1] + return lon, lat + + def inv_transform_xy(x, y): + x, y = np.asarray(x), np.asarray(y) + ll1 = np.concatenate((x[:,np.newaxis], y[:,np.newaxis]), 1) + ll2 = aux_trans.inverted().transform(ll1) + lon, lat = ll2[:,0], ll2[:,1] + return lon, lat + + else: + transform_xy, inv_transform_xy = aux_trans + + self.transform_xy = transform_xy + self.inv_transform_xy = inv_transform_xy + + + def update(self, **kw): + for k in kw: + if k in ["extreme_finder", + "grid_locator1", + "grid_locator2", + "tick_formatter1", + "tick_formatter2"]: + setattr(self, k, kw[k]) + else: + raise ValueError("unknown update property '%s'" % k) + + +class GridFinder(GridFinderBase): + + def __init__(self, + transform, + extreme_finder=None, + grid_locator1=None, + grid_locator2=None, + tick_formatter1=None, + tick_formatter2=None): + """ + transform : transform from the image coordinate (which will be + the transData of the axes to the world coordinate. + + or transform = (transform_xy, inv_transform_xy) + + locator1, locator2 : grid locator for 1st and 2nd axis. + """ + if extreme_finder is None: + extreme_finder = ExtremeFinderSimple(20, 20) + if grid_locator1 is None: + grid_locator1 = MaxNLocator() + if grid_locator2 is None: + grid_locator2 = MaxNLocator() + if tick_formatter1 is None: + tick_formatter1 = FormatterPrettyPrint() + if tick_formatter2 is None: + tick_formatter2 = FormatterPrettyPrint() + super(GridFinder, self).__init__( + extreme_finder, + grid_locator1, + grid_locator2, + tick_formatter1, + tick_formatter2) + self.update_transform(transform) + + +class MaxNLocator(mticker.MaxNLocator): + def __init__(self, nbins=10, steps=None, + trim=True, + integer=False, + symmetric=False, + prune=None): + # trim argument has no effect. It has been left for API compatibility + mticker.MaxNLocator.__init__(self, nbins, steps=steps, + integer=integer, + symmetric=symmetric, prune=prune) + self.create_dummy_axis() + self._factor = None + + def __call__(self, v1, v2): + if self._factor is not None: + self.set_bounds(v1*self._factor, v2*self._factor) + locs = mticker.MaxNLocator.__call__(self) + return np.array(locs), len(locs), self._factor + else: + self.set_bounds(v1, v2) + locs = mticker.MaxNLocator.__call__(self) + return np.array(locs), len(locs), None + + def set_factor(self, f): + self._factor = f + + +class FixedLocator(object): + def __init__(self, locs): + self._locs = locs + self._factor = None + + + def __call__(self, v1, v2): + if self._factor is None: + v1, v2 = sorted([v1, v2]) + else: + v1, v2 = sorted([v1*self._factor, v2*self._factor]) + locs = np.array([l for l in self._locs if ((v1 <= l) and (l <= v2))]) + return locs, len(locs), self._factor + + def set_factor(self, f): + self._factor = f + + + +# Tick Formatter + +class FormatterPrettyPrint(object): + def __init__(self, useMathText=True): + self._fmt = mticker.ScalarFormatter( + useMathText=useMathText, useOffset=False) + self._fmt.create_dummy_axis() + self._ignore_factor = True + + def __call__(self, direction, factor, values): + if not self._ignore_factor: + if factor is None: + factor = 1. + values = [v/factor for v in values] + #values = [v for v in values] + self._fmt.set_locs(values) + return [self._fmt(v) for v in values] + + +class DictFormatter(object): + def __init__(self, format_dict, formatter=None): + """ + format_dict : dictionary for format strings to be used. + formatter : fall-back formatter + """ + super(DictFormatter, self).__init__() + self._format_dict = format_dict + self._fallback_formatter = formatter + + def __call__(self, direction, factor, values): + """ + factor is ignored if value is found in the dictionary + """ + + if self._fallback_formatter: + fallback_strings = self._fallback_formatter( + direction, factor, values) + else: + fallback_strings = [""]*len(values) + + r = [self._format_dict.get(k, v) for k, v in zip(values, + fallback_strings)] + return r diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/grid_helper_curvelinear.py b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/grid_helper_curvelinear.py new file mode 100644 index 0000000000..578645148e --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/grid_helper_curvelinear.py @@ -0,0 +1,475 @@ +""" +An experimental support for curvilinear grid. +""" +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six +from six.moves import zip + +from itertools import chain +from .grid_finder import GridFinder + +from .axislines import AxisArtistHelper, GridHelperBase +from .axis_artist import AxisArtist +from matplotlib.transforms import Affine2D, IdentityTransform +import numpy as np + +from matplotlib.path import Path + +class FixedAxisArtistHelper(AxisArtistHelper.Fixed): + """ + Helper class for a fixed axis. + """ + + def __init__(self, grid_helper, side, nth_coord_ticks=None): + """ + nth_coord = along which coordinate value varies. + nth_coord = 0 -> x axis, nth_coord = 1 -> y axis + """ + + super(FixedAxisArtistHelper, self).__init__(loc=side) + + self.grid_helper = grid_helper + if nth_coord_ticks is None: + nth_coord_ticks = self.nth_coord + self.nth_coord_ticks = nth_coord_ticks + + self.side = side + self._limits_inverted = False + + def update_lim(self, axes): + self.grid_helper.update_lim(axes) + + if self.nth_coord == 0: + xy1, xy2 = axes.get_ylim() + else: + xy1, xy2 = axes.get_xlim() + + if xy1 > xy2: + self._limits_inverted = True + else: + self._limits_inverted = False + + + def change_tick_coord(self, coord_number=None): + if coord_number is None: + self.nth_coord_ticks = 1 - self.nth_coord_ticks + elif coord_number in [0, 1]: + self.nth_coord_ticks = coord_number + else: + raise Exception("wrong coord number") + + + def get_tick_transform(self, axes): + return axes.transData + + def get_tick_iterators(self, axes): + """tick_loc, tick_angle, tick_label""" + + g = self.grid_helper + + if self._limits_inverted: + side = {"left":"right","right":"left", + "top":"bottom", "bottom":"top"}[self.side] + else: + side = self.side + + ti1 = g.get_tick_iterator(self.nth_coord_ticks, side) + ti2 = g.get_tick_iterator(1-self.nth_coord_ticks, side, minor=True) + + #ti2 = g.get_tick_iterator(1-self.nth_coord_ticks, self.side, minor=True) + + return chain(ti1, ti2), iter([]) + + + +class FloatingAxisArtistHelper(AxisArtistHelper.Floating): + + def __init__(self, grid_helper, nth_coord, value, axis_direction=None): + """ + nth_coord = along which coordinate value varies. + nth_coord = 0 -> x axis, nth_coord = 1 -> y axis + """ + + super(FloatingAxisArtistHelper, self).__init__(nth_coord, + value, + ) + self.value = value + self.grid_helper = grid_helper + self._extremes = None, None + + self._get_line_path = None # a method that returns a Path. + self._line_num_points = 100 # number of points to create a line + + def set_extremes(self, e1, e2): + self._extremes = e1, e2 + + def update_lim(self, axes): + self.grid_helper.update_lim(axes) + + x1, x2 = axes.get_xlim() + y1, y2 = axes.get_ylim() + grid_finder = self.grid_helper.grid_finder + extremes = grid_finder.extreme_finder(grid_finder.inv_transform_xy, + x1, y1, x2, y2) + + extremes = list(extremes) + e1, e2 = self._extremes # ranges of other coordinates + if self.nth_coord == 0: + if e1 is not None: + extremes[2] = max(e1, extremes[2]) + if e2 is not None: + extremes[3] = min(e2, extremes[3]) + elif self.nth_coord == 1: + if e1 is not None: + extremes[0] = max(e1, extremes[0]) + if e2 is not None: + extremes[1] = min(e2, extremes[1]) + + grid_info = dict() + lon_min, lon_max, lat_min, lat_max = extremes + lon_levs, lon_n, lon_factor = \ + grid_finder.grid_locator1(lon_min, lon_max) + lat_levs, lat_n, lat_factor = \ + grid_finder.grid_locator2(lat_min, lat_max) + grid_info["extremes"] = extremes + + grid_info["lon_info"] = lon_levs, lon_n, lon_factor + grid_info["lat_info"] = lat_levs, lat_n, lat_factor + + grid_info["lon_labels"] = grid_finder.tick_formatter1("bottom", + lon_factor, + lon_levs) + + grid_info["lat_labels"] = grid_finder.tick_formatter2("bottom", + lat_factor, + lat_levs) + + grid_finder = self.grid_helper.grid_finder + + #e1, e2 = self._extremes # ranges of other coordinates + if self.nth_coord == 0: + xx0 = np.linspace(self.value, self.value, self._line_num_points) + yy0 = np.linspace(extremes[2], extremes[3], self._line_num_points) + xx, yy = grid_finder.transform_xy(xx0, yy0) + elif self.nth_coord == 1: + xx0 = np.linspace(extremes[0], extremes[1], self._line_num_points) + yy0 = np.linspace(self.value, self.value, self._line_num_points) + xx, yy = grid_finder.transform_xy(xx0, yy0) + + grid_info["line_xy"] = xx, yy + self.grid_info = grid_info + + def get_axislabel_transform(self, axes): + return Affine2D() #axes.transData + + def get_axislabel_pos_angle(self, axes): + + extremes = self.grid_info["extremes"] + + if self.nth_coord == 0: + xx0 = self.value + yy0 = (extremes[2]+extremes[3])/2. + dxx, dyy = 0., abs(extremes[2]-extremes[3])/1000. + elif self.nth_coord == 1: + xx0 = (extremes[0]+extremes[1])/2. + yy0 = self.value + dxx, dyy = abs(extremes[0]-extremes[1])/1000., 0. + + grid_finder = self.grid_helper.grid_finder + xx1, yy1 = grid_finder.transform_xy([xx0], [yy0]) + + trans_passingthrough_point = axes.transData + axes.transAxes.inverted() + p = trans_passingthrough_point.transform_point([xx1[0], yy1[0]]) + + + if (0. <= p[0] <= 1.) and (0. <= p[1] <= 1.): + xx1c, yy1c = axes.transData.transform_point([xx1[0], yy1[0]]) + xx2, yy2 = grid_finder.transform_xy([xx0+dxx], [yy0+dyy]) + xx2c, yy2c = axes.transData.transform_point([xx2[0], yy2[0]]) + + return (xx1c, yy1c), np.arctan2(yy2c-yy1c, xx2c-xx1c)/np.pi*180. + else: + return None, None + + + + + def get_tick_transform(self, axes): + return IdentityTransform() #axes.transData + + def get_tick_iterators(self, axes): + """tick_loc, tick_angle, tick_label, (optionally) tick_label""" + + grid_finder = self.grid_helper.grid_finder + + lat_levs, lat_n, lat_factor = self.grid_info["lat_info"] + lat_levs = np.asarray(lat_levs) + if lat_factor is not None: + yy0 = lat_levs / lat_factor + dy = 0.01 / lat_factor + else: + yy0 = lat_levs + dy = 0.01 + + lon_levs, lon_n, lon_factor = self.grid_info["lon_info"] + lon_levs = np.asarray(lon_levs) + if lon_factor is not None: + xx0 = lon_levs / lon_factor + dx = 0.01 / lon_factor + else: + xx0 = lon_levs + dx = 0.01 + + if None in self._extremes: + e0, e1 = self._extremes + else: + e0, e1 = sorted(self._extremes) + if e0 is None: + e0 = -np.inf + if e1 is None: + e1 = np.inf + + if self.nth_coord == 0: + mask = (e0 <= yy0) & (yy0 <= e1) + #xx0, yy0 = xx0[mask], yy0[mask] + yy0 = yy0[mask] + elif self.nth_coord == 1: + mask = (e0 <= xx0) & (xx0 <= e1) + #xx0, yy0 = xx0[mask], yy0[mask] + xx0 = xx0[mask] + + def transform_xy(x, y): + x1, y1 = grid_finder.transform_xy(x, y) + x2y2 = axes.transData.transform(np.array([x1, y1]).transpose()) + x2, y2 = x2y2.transpose() + return x2, y2 + + # find angles + if self.nth_coord == 0: + xx0 = np.empty_like(yy0) + xx0.fill(self.value) + + xx1, yy1 = transform_xy(xx0, yy0) + + xx00 = xx0.copy() + xx00[xx0+dx>e1] -= dx + xx1a, yy1a = transform_xy(xx00, yy0) + xx1b, yy1b = transform_xy(xx00+dx, yy0) + + xx2a, yy2a = transform_xy(xx0, yy0) + xx2b, yy2b = transform_xy(xx0, yy0+dy) + + labels = self.grid_info["lat_labels"] + labels = [l for l, m in zip(labels, mask) if m] + + elif self.nth_coord == 1: + yy0 = np.empty_like(xx0) + yy0.fill(self.value) + + xx1, yy1 = transform_xy(xx0, yy0) + + xx1a, yy1a = transform_xy(xx0, yy0) + xx1b, yy1b = transform_xy(xx0, yy0+dy) + + xx00 = xx0.copy() + xx00[xx0+dx>e1] -= dx + xx2a, yy2a = transform_xy(xx00, yy0) + xx2b, yy2b = transform_xy(xx00+dx, yy0) + + labels = self.grid_info["lon_labels"] + labels = [l for l, m in zip(labels, mask) if m] + + + def f1(): + dd = np.arctan2(yy1b-yy1a, xx1b-xx1a) # angle normal + dd2 = np.arctan2(yy2b-yy2a, xx2b-xx2a) # angle tangent + mm = ((yy1b-yy1a)==0.) & ((xx1b-xx1a)==0.) # mask where dd1 is not defined + dd[mm] = dd2[mm] + np.pi / 2 + #dd = np.arctan2(yy2-yy1, xx2-xx1) # angle normal + #dd2 = np.arctan2(yy3-yy1, xx3-xx1) # angle tangent + #mm = ((yy2-yy1)==0.) & ((xx2-xx1)==0.) # mask where dd1 is not defined + #dd[mm] = dd2[mm] + np.pi / 2 + + #dd += np.pi + + #dd = np.arctan2(xx2-xx1, angle_tangent-yy1) + trans_tick = self.get_tick_transform(axes) + tr2ax = trans_tick + axes.transAxes.inverted() + for x, y, d, d2, lab in zip(xx1, yy1, dd, dd2, labels): + c2 = tr2ax.transform_point((x, y)) + delta=0.00001 + if (0. -delta<= c2[0] <= 1.+delta) and \ + (0. -delta<= c2[1] <= 1.+delta): + d1 = d/3.14159*180. + d2 = d2/3.14159*180. + yield [x, y], d1, d2, lab + + return f1(), iter([]) + + def get_line_transform(self, axes): + return axes.transData + + def get_line(self, axes): + self.update_lim(axes) + x, y = self.grid_info["line_xy"] + + if self._get_line_path is None: + return Path(np.column_stack([x, y])) + else: + return self._get_line_path(axes, x, y) + + + + +class GridHelperCurveLinear(GridHelperBase): + + def __init__(self, aux_trans, + extreme_finder=None, + grid_locator1=None, + grid_locator2=None, + tick_formatter1=None, + tick_formatter2=None): + """ + aux_trans : a transform from the source (curved) coordinate to + target (rectilinear) coordinate. An instance of MPL's Transform + (inverse transform should be defined) or a tuple of two callable + objects which defines the transform and its inverse. The callables + need take two arguments of array of source coordinates and + should return two target coordinates. + + e.g., ``x2, y2 = trans(x1, y1)`` + """ + super(GridHelperCurveLinear, self).__init__() + + self.grid_info = None + self._old_values = None + #self._grid_params = dict() + self._aux_trans = aux_trans + + self.grid_finder = GridFinder(aux_trans, + extreme_finder, + grid_locator1, + grid_locator2, + tick_formatter1, + tick_formatter2) + + + def update_grid_finder(self, aux_trans=None, **kw): + + if aux_trans is not None: + self.grid_finder.update_transform(aux_trans) + + self.grid_finder.update(**kw) + self.invalidate() + + + def _update(self, x1, x2, y1, y2): + "bbox in 0-based image coordinates" + # update wcsgrid + + if self.valid() and self._old_values == (x1, x2, y1, y2): + return + + self._update_grid(x1, y1, x2, y2) + + self._old_values = (x1, x2, y1, y2) + + self._force_update = False + + + def new_fixed_axis(self, loc, + nth_coord=None, + axis_direction=None, + offset=None, + axes=None): + + + if axes is None: + axes = self.axes + + if axis_direction is None: + axis_direction = loc + _helper = FixedAxisArtistHelper(self, loc, + #nth_coord, + nth_coord_ticks=nth_coord, + ) + + axisline = AxisArtist(axes, _helper, axis_direction=axis_direction) + + return axisline + + + def new_floating_axis(self, nth_coord, + value, + axes=None, + axis_direction="bottom" + ): + + if axes is None: + axes = self.axes + + _helper = FloatingAxisArtistHelper( + self, nth_coord, value, axis_direction) + + axisline = AxisArtist(axes, _helper) + + #_helper = FloatingAxisArtistHelper(self, nth_coord, + # value, + # label_direction=label_direction, + # ) + + #axisline = AxisArtistFloating(axes, _helper, + # axis_direction=axis_direction) + axisline.line.set_clip_on(True) + axisline.line.set_clip_box(axisline.axes.bbox) + #axisline.major_ticklabels.set_visible(True) + #axisline.minor_ticklabels.set_visible(False) + + #axisline.major_ticklabels.set_rotate_along_line(True) + #axisline.set_rotate_label_along_line(True) + + return axisline + + + def _update_grid(self, x1, y1, x2, y2): + + self.grid_info = self.grid_finder.get_grid_info(x1, y1, x2, y2) + + + def get_gridlines(self, which="major", axis="both"): + grid_lines = [] + + if axis in ["both", "x"]: + for gl in self.grid_info["lon"]["lines"]: + grid_lines.extend(gl) + if axis in ["both", "y"]: + for gl in self.grid_info["lat"]["lines"]: + grid_lines.extend(gl) + + return grid_lines + + + def get_tick_iterator(self, nth_coord, axis_side, minor=False): + + #axisnr = dict(left=0, bottom=1, right=2, top=3)[axis_side] + angle_tangent = dict(left=90, right=90, bottom=0, top=0)[axis_side] + #angle = [0, 90, 180, 270][axisnr] + lon_or_lat = ["lon", "lat"][nth_coord] + if not minor: # major ticks + def f(): + for (xy, a), l in zip(self.grid_info[lon_or_lat]["tick_locs"][axis_side], + self.grid_info[lon_or_lat]["tick_labels"][axis_side]): + angle_normal = a + yield xy, angle_normal, angle_tangent, l + else: + def f(): + for (xy, a), l in zip(self.grid_info[lon_or_lat]["tick_locs"][axis_side], + self.grid_info[lon_or_lat]["tick_labels"][axis_side]): + angle_normal = a + yield xy, angle_normal, angle_tangent, "" + #for xy, a, l in self.grid_info[lon_or_lat]["ticks"][axis_side]: + # yield xy, a, "" + + return f() diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/parasite_axes.py b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/parasite_axes.py new file mode 100644 index 0000000000..cad56e43a2 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/axisartist/parasite_axes.py @@ -0,0 +1,18 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +from mpl_toolkits.axes_grid1.parasite_axes import ( + host_axes_class_factory, parasite_axes_class_factory, + parasite_axes_auxtrans_class_factory, subplot_class_factory) + +from .axislines import Axes + + +ParasiteAxes = parasite_axes_class_factory(Axes) + +ParasiteAxesAuxTrans = \ + parasite_axes_auxtrans_class_factory(axes_class=ParasiteAxes) + +HostAxes = host_axes_class_factory(axes_class=Axes) + +SubplotHost = subplot_class_factory(HostAxes) diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/__init__.py b/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/__init__.py new file mode 100644 index 0000000000..cd9c2139d2 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/__init__.py @@ -0,0 +1,6 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six + +from .axes3d import Axes3D diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/art3d.py b/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/art3d.py new file mode 100644 index 0000000000..ef55dd693e --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/art3d.py @@ -0,0 +1,774 @@ +# art3d.py, original mplot3d version by John Porter +# Parts rewritten by Reinier Heeres <reinier@heeres.eu> +# Minor additions by Ben Axelrod <baxelrod@coroware.com> + +''' +Module containing 3D artist code and functions to convert 2D +artists into 3D versions which can be added to an Axes3D. +''' +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six +from six.moves import zip + +import math + +import numpy as np + +from matplotlib import ( + artist, cbook, colors as mcolors, lines, text as mtext, path as mpath) +from matplotlib.cbook import _backports +from matplotlib.collections import ( + Collection, LineCollection, PolyCollection, PatchCollection, + PathCollection) +from matplotlib.colors import Normalize +from matplotlib.patches import Patch +from . import proj3d + + +def norm_angle(a): + """Return angle between -180 and +180""" + a = (a + 360) % 360 + if a > 180: + a = a - 360 + return a + + +def norm_text_angle(a): + """Return angle between -90 and +90""" + a = (a + 180) % 180 + if a > 90: + a = a - 180 + return a + + +def get_dir_vector(zdir): + if zdir == 'x': + return np.array((1, 0, 0)) + elif zdir == 'y': + return np.array((0, 1, 0)) + elif zdir == 'z': + return np.array((0, 0, 1)) + elif zdir is None: + return np.array((0, 0, 0)) + elif cbook.iterable(zdir) and len(zdir) == 3: + return zdir + else: + raise ValueError("'x', 'y', 'z', None or vector of length 3 expected") + + +class Text3D(mtext.Text): + ''' + Text object with 3D position and (in the future) direction. + ''' + + def __init__(self, x=0, y=0, z=0, text='', zdir='z', **kwargs): + ''' + *x*, *y*, *z* Position of text + *text* Text string to display + *zdir* Direction of text + + Keyword arguments are passed onto :func:`~matplotlib.text.Text`. + ''' + mtext.Text.__init__(self, x, y, text, **kwargs) + self.set_3d_properties(z, zdir) + + def set_3d_properties(self, z=0, zdir='z'): + x, y = self.get_position() + self._position3d = np.array((x, y, z)) + self._dir_vec = get_dir_vector(zdir) + self.stale = True + + def draw(self, renderer): + proj = proj3d.proj_trans_points( + [self._position3d, self._position3d + self._dir_vec], renderer.M) + dx = proj[0][1] - proj[0][0] + dy = proj[1][1] - proj[1][0] + if dx==0. and dy==0.: + # atan2 raises ValueError: math domain error on 0,0 + angle = 0. + else: + angle = math.degrees(math.atan2(dy, dx)) + self.set_position((proj[0][0], proj[1][0])) + self.set_rotation(norm_text_angle(angle)) + mtext.Text.draw(self, renderer) + self.stale = False + + +def text_2d_to_3d(obj, z=0, zdir='z'): + """Convert a Text to a Text3D object.""" + obj.__class__ = Text3D + obj.set_3d_properties(z, zdir) + + +class Line3D(lines.Line2D): + ''' + 3D line object. + ''' + + def __init__(self, xs, ys, zs, *args, **kwargs): + ''' + Keyword arguments are passed onto :func:`~matplotlib.lines.Line2D`. + ''' + lines.Line2D.__init__(self, [], [], *args, **kwargs) + self._verts3d = xs, ys, zs + + def set_3d_properties(self, zs=0, zdir='z'): + xs = self.get_xdata() + ys = self.get_ydata() + + try: + # If *zs* is a list or array, then this will fail and + # just proceed to juggle_axes(). + zs = float(zs) + zs = [zs for x in xs] + except TypeError: + pass + self._verts3d = juggle_axes(xs, ys, zs, zdir) + self.stale = True + + def draw(self, renderer): + xs3d, ys3d, zs3d = self._verts3d + xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, renderer.M) + self.set_data(xs, ys) + lines.Line2D.draw(self, renderer) + self.stale = False + + +def line_2d_to_3d(line, zs=0, zdir='z'): + ''' + Convert a 2D line to 3D. + ''' + line.__class__ = Line3D + line.set_3d_properties(zs, zdir) + + +def path_to_3d_segment(path, zs=0, zdir='z'): + '''Convert a path to a 3D segment.''' + + zs = _backports.broadcast_to(zs, len(path)) + pathsegs = path.iter_segments(simplify=False, curves=False) + seg = [(x, y, z) for (((x, y), code), z) in zip(pathsegs, zs)] + seg3d = [juggle_axes(x, y, z, zdir) for (x, y, z) in seg] + return seg3d + + +def paths_to_3d_segments(paths, zs=0, zdir='z'): + ''' + Convert paths from a collection object to 3D segments. + ''' + + zs = _backports.broadcast_to(zs, len(paths)) + segs = [path_to_3d_segment(path, pathz, zdir) + for path, pathz in zip(paths, zs)] + return segs + + +def path_to_3d_segment_with_codes(path, zs=0, zdir='z'): + '''Convert a path to a 3D segment with path codes.''' + + zs = _backports.broadcast_to(zs, len(path)) + seg = [] + codes = [] + pathsegs = path.iter_segments(simplify=False, curves=False) + for (((x, y), code), z) in zip(pathsegs, zs): + seg.append((x, y, z)) + codes.append(code) + seg3d = [juggle_axes(x, y, z, zdir) for (x, y, z) in seg] + return seg3d, codes + + +def paths_to_3d_segments_with_codes(paths, zs=0, zdir='z'): + ''' + Convert paths from a collection object to 3D segments with path codes. + ''' + + zs = _backports.broadcast_to(zs, len(paths)) + segments = [] + codes_list = [] + for path, pathz in zip(paths, zs): + segs, codes = path_to_3d_segment_with_codes(path, pathz, zdir) + segments.append(segs) + codes_list.append(codes) + return segments, codes_list + + +class Line3DCollection(LineCollection): + ''' + A collection of 3D lines. + ''' + + def __init__(self, segments, *args, **kwargs): + ''' + Keyword arguments are passed onto :func:`~matplotlib.collections.LineCollection`. + ''' + LineCollection.__init__(self, segments, *args, **kwargs) + + def set_sort_zpos(self, val): + '''Set the position to use for z-sorting.''' + self._sort_zpos = val + self.stale = True + + def set_segments(self, segments): + ''' + Set 3D segments + ''' + self._segments3d = np.asanyarray(segments) + LineCollection.set_segments(self, []) + + def do_3d_projection(self, renderer): + ''' + Project the points according to renderer matrix. + ''' + xyslist = [ + proj3d.proj_trans_points(points, renderer.M) for points in + self._segments3d] + segments_2d = [np.column_stack([xs, ys]) for xs, ys, zs in xyslist] + LineCollection.set_segments(self, segments_2d) + + # FIXME + minz = 1e9 + for xs, ys, zs in xyslist: + minz = min(minz, min(zs)) + return minz + + def draw(self, renderer, project=False): + if project: + self.do_3d_projection(renderer) + LineCollection.draw(self, renderer) + + +def line_collection_2d_to_3d(col, zs=0, zdir='z'): + """Convert a LineCollection to a Line3DCollection object.""" + segments3d = paths_to_3d_segments(col.get_paths(), zs, zdir) + col.__class__ = Line3DCollection + col.set_segments(segments3d) + + +class Patch3D(Patch): + ''' + 3D patch object. + ''' + + def __init__(self, *args, **kwargs): + zs = kwargs.pop('zs', []) + zdir = kwargs.pop('zdir', 'z') + Patch.__init__(self, *args, **kwargs) + self.set_3d_properties(zs, zdir) + + def set_3d_properties(self, verts, zs=0, zdir='z'): + zs = _backports.broadcast_to(zs, len(verts)) + self._segment3d = [juggle_axes(x, y, z, zdir) + for ((x, y), z) in zip(verts, zs)] + self._facecolor3d = Patch.get_facecolor(self) + + def get_path(self): + return self._path2d + + def get_facecolor(self): + return self._facecolor2d + + def do_3d_projection(self, renderer): + s = self._segment3d + xs, ys, zs = zip(*s) + vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, renderer.M) + self._path2d = mpath.Path(np.column_stack([vxs, vys])) + # FIXME: coloring + self._facecolor2d = self._facecolor3d + return min(vzs) + + def draw(self, renderer): + Patch.draw(self, renderer) + + +class PathPatch3D(Patch3D): + ''' + 3D PathPatch object. + ''' + + def __init__(self, path, **kwargs): + zs = kwargs.pop('zs', []) + zdir = kwargs.pop('zdir', 'z') + Patch.__init__(self, **kwargs) + self.set_3d_properties(path, zs, zdir) + + def set_3d_properties(self, path, zs=0, zdir='z'): + Patch3D.set_3d_properties(self, path.vertices, zs=zs, zdir=zdir) + self._code3d = path.codes + + def do_3d_projection(self, renderer): + s = self._segment3d + xs, ys, zs = zip(*s) + vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, renderer.M) + self._path2d = mpath.Path(np.column_stack([vxs, vys]), self._code3d) + # FIXME: coloring + self._facecolor2d = self._facecolor3d + return min(vzs) + + +def get_patch_verts(patch): + """Return a list of vertices for the path of a patch.""" + trans = patch.get_patch_transform() + path = patch.get_path() + polygons = path.to_polygons(trans) + if len(polygons): + return polygons[0] + else: + return [] + + +def patch_2d_to_3d(patch, z=0, zdir='z'): + """Convert a Patch to a Patch3D object.""" + verts = get_patch_verts(patch) + patch.__class__ = Patch3D + patch.set_3d_properties(verts, z, zdir) + + +def pathpatch_2d_to_3d(pathpatch, z=0, zdir='z'): + """Convert a PathPatch to a PathPatch3D object.""" + path = pathpatch.get_path() + trans = pathpatch.get_patch_transform() + + mpath = trans.transform_path(path) + pathpatch.__class__ = PathPatch3D + pathpatch.set_3d_properties(mpath, z, zdir) + + +class Patch3DCollection(PatchCollection): + ''' + A collection of 3D patches. + ''' + + def __init__(self, *args, **kwargs): + """ + Create a collection of flat 3D patches with its normal vector + pointed in *zdir* direction, and located at *zs* on the *zdir* + axis. 'zs' can be a scalar or an array-like of the same length as + the number of patches in the collection. + + Constructor arguments are the same as for + :class:`~matplotlib.collections.PatchCollection`. In addition, + keywords *zs=0* and *zdir='z'* are available. + + Also, the keyword argument "depthshade" is available to + indicate whether or not to shade the patches in order to + give the appearance of depth (default is *True*). + This is typically desired in scatter plots. + """ + zs = kwargs.pop('zs', 0) + zdir = kwargs.pop('zdir', 'z') + self._depthshade = kwargs.pop('depthshade', True) + PatchCollection.__init__(self, *args, **kwargs) + self.set_3d_properties(zs, zdir) + + def set_sort_zpos(self, val): + '''Set the position to use for z-sorting.''' + self._sort_zpos = val + self.stale = True + + def set_3d_properties(self, zs, zdir): + # Force the collection to initialize the face and edgecolors + # just in case it is a scalarmappable with a colormap. + self.update_scalarmappable() + offsets = self.get_offsets() + if len(offsets) > 0: + xs, ys = zip(*offsets) + else: + xs = [] + ys = [] + self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir) + self._facecolor3d = self.get_facecolor() + self._edgecolor3d = self.get_edgecolor() + self.stale = True + + def do_3d_projection(self, renderer): + xs, ys, zs = self._offsets3d + vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, renderer.M) + + fcs = (zalpha(self._facecolor3d, vzs) if self._depthshade else + self._facecolor3d) + fcs = mcolors.to_rgba_array(fcs, self._alpha) + self.set_facecolors(fcs) + + ecs = (zalpha(self._edgecolor3d, vzs) if self._depthshade else + self._edgecolor3d) + ecs = mcolors.to_rgba_array(ecs, self._alpha) + self.set_edgecolors(ecs) + PatchCollection.set_offsets(self, np.column_stack([vxs, vys])) + + if vzs.size > 0: + return min(vzs) + else: + return np.nan + + +class Path3DCollection(PathCollection): + ''' + A collection of 3D paths. + ''' + + def __init__(self, *args, **kwargs): + """ + Create a collection of flat 3D paths with its normal vector + pointed in *zdir* direction, and located at *zs* on the *zdir* + axis. 'zs' can be a scalar or an array-like of the same length as + the number of paths in the collection. + + Constructor arguments are the same as for + :class:`~matplotlib.collections.PathCollection`. In addition, + keywords *zs=0* and *zdir='z'* are available. + + Also, the keyword argument "depthshade" is available to + indicate whether or not to shade the patches in order to + give the appearance of depth (default is *True*). + This is typically desired in scatter plots. + """ + zs = kwargs.pop('zs', 0) + zdir = kwargs.pop('zdir', 'z') + self._depthshade = kwargs.pop('depthshade', True) + PathCollection.__init__(self, *args, **kwargs) + self.set_3d_properties(zs, zdir) + + def set_sort_zpos(self, val): + '''Set the position to use for z-sorting.''' + self._sort_zpos = val + self.stale = True + + def set_3d_properties(self, zs, zdir): + # Force the collection to initialize the face and edgecolors + # just in case it is a scalarmappable with a colormap. + self.update_scalarmappable() + offsets = self.get_offsets() + if len(offsets) > 0: + xs, ys = zip(*offsets) + else: + xs = [] + ys = [] + self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir) + self._facecolor3d = self.get_facecolor() + self._edgecolor3d = self.get_edgecolor() + self.stale = True + + def do_3d_projection(self, renderer): + xs, ys, zs = self._offsets3d + vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, renderer.M) + + fcs = (zalpha(self._facecolor3d, vzs) if self._depthshade else + self._facecolor3d) + fcs = mcolors.to_rgba_array(fcs, self._alpha) + self.set_facecolors(fcs) + + ecs = (zalpha(self._edgecolor3d, vzs) if self._depthshade else + self._edgecolor3d) + ecs = mcolors.to_rgba_array(ecs, self._alpha) + self.set_edgecolors(ecs) + PathCollection.set_offsets(self, np.column_stack([vxs, vys])) + + if vzs.size > 0 : + return min(vzs) + else : + return np.nan + + +def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True): + """ + Convert a :class:`~matplotlib.collections.PatchCollection` into a + :class:`Patch3DCollection` object + (or a :class:`~matplotlib.collections.PathCollection` into a + :class:`Path3DCollection` object). + + Keywords: + + *za* The location or locations to place the patches in the + collection along the *zdir* axis. Defaults to 0. + + *zdir* The axis in which to place the patches. Default is "z". + + *depthshade* Whether to shade the patches to give a sense of depth. + Defaults to *True*. + + """ + if isinstance(col, PathCollection): + col.__class__ = Path3DCollection + elif isinstance(col, PatchCollection): + col.__class__ = Patch3DCollection + col._depthshade = depthshade + col.set_3d_properties(zs, zdir) + + +class Poly3DCollection(PolyCollection): + ''' + A collection of 3D polygons. + ''' + + def __init__(self, verts, *args, **kwargs): + ''' + Create a Poly3DCollection. + + *verts* should contain 3D coordinates. + + Keyword arguments: + zsort, see set_zsort for options. + + Note that this class does a bit of magic with the _facecolors + and _edgecolors properties. + ''' + zsort = kwargs.pop('zsort', True) + PolyCollection.__init__(self, verts, *args, **kwargs) + self.set_zsort(zsort) + self._codes3d = None + + _zsort_functions = { + 'average': np.average, + 'min': np.min, + 'max': np.max, + } + + def set_zsort(self, zsort): + ''' + Set z-sorting behaviour: + boolean: if True use default 'average' + string: 'average', 'min' or 'max' + ''' + + if zsort is True: + zsort = 'average' + + if zsort is not False: + if zsort in self._zsort_functions: + zsortfunc = self._zsort_functions[zsort] + else: + return False + else: + zsortfunc = None + + self._zsort = zsort + self._sort_zpos = None + self._zsortfunc = zsortfunc + self.stale = True + + def get_vector(self, segments3d): + """Optimize points for projection""" + si = 0 + ei = 0 + segis = [] + points = [] + for p in segments3d: + points.extend(p) + ei = si + len(p) + segis.append((si, ei)) + si = ei + + if len(segments3d): + xs, ys, zs = zip(*points) + else : + # We need this so that we can skip the bad unpacking from zip() + xs, ys, zs = [], [], [] + + ones = np.ones(len(xs)) + self._vec = np.array([xs, ys, zs, ones]) + self._segis = segis + + def set_verts(self, verts, closed=True): + '''Set 3D vertices.''' + self.get_vector(verts) + # 2D verts will be updated at draw time + PolyCollection.set_verts(self, [], False) + self._closed = closed + + def set_verts_and_codes(self, verts, codes): + '''Sets 3D vertices with path codes''' + # set vertices with closed=False to prevent PolyCollection from + # setting path codes + self.set_verts(verts, closed=False) + # and set our own codes instead. + self._codes3d = codes + + def set_3d_properties(self): + # Force the collection to initialize the face and edgecolors + # just in case it is a scalarmappable with a colormap. + self.update_scalarmappable() + self._sort_zpos = None + self.set_zsort(True) + self._facecolors3d = PolyCollection.get_facecolors(self) + self._edgecolors3d = PolyCollection.get_edgecolors(self) + self._alpha3d = PolyCollection.get_alpha(self) + self.stale = True + + def set_sort_zpos(self,val): + '''Set the position to use for z-sorting.''' + self._sort_zpos = val + self.stale = True + + def do_3d_projection(self, renderer): + ''' + Perform the 3D projection for this object. + ''' + # FIXME: This may no longer be needed? + if self._A is not None: + self.update_scalarmappable() + self._facecolors3d = self._facecolors + + txs, tys, tzs = proj3d.proj_transform_vec(self._vec, renderer.M) + xyzlist = [(txs[si:ei], tys[si:ei], tzs[si:ei]) + for si, ei in self._segis] + + # This extra fuss is to re-order face / edge colors + cface = self._facecolors3d + cedge = self._edgecolors3d + if len(cface) != len(xyzlist): + cface = cface.repeat(len(xyzlist), axis=0) + if len(cedge) != len(xyzlist): + if len(cedge) == 0: + cedge = cface + else: + cedge = cedge.repeat(len(xyzlist), axis=0) + + # if required sort by depth (furthest drawn first) + if self._zsort: + z_segments_2d = sorted( + ((self._zsortfunc(zs), np.column_stack([xs, ys]), fc, ec, idx) + for idx, ((xs, ys, zs), fc, ec) + in enumerate(zip(xyzlist, cface, cedge))), + key=lambda x: x[0], reverse=True) + else: + raise ValueError("whoops") + + segments_2d = [s for z, s, fc, ec, idx in z_segments_2d] + if self._codes3d is not None: + codes = [self._codes3d[idx] for z, s, fc, ec, idx in z_segments_2d] + PolyCollection.set_verts_and_codes(self, segments_2d, codes) + else: + PolyCollection.set_verts(self, segments_2d, self._closed) + + self._facecolors2d = [fc for z, s, fc, ec, idx in z_segments_2d] + if len(self._edgecolors3d) == len(cface): + self._edgecolors2d = [ec for z, s, fc, ec, idx in z_segments_2d] + else: + self._edgecolors2d = self._edgecolors3d + + # Return zorder value + if self._sort_zpos is not None: + zvec = np.array([[0], [0], [self._sort_zpos], [1]]) + ztrans = proj3d.proj_transform_vec(zvec, renderer.M) + return ztrans[2][0] + elif tzs.size > 0 : + # FIXME: Some results still don't look quite right. + # In particular, examine contourf3d_demo2.py + # with az = -54 and elev = -45. + return np.min(tzs) + else : + return np.nan + + def set_facecolor(self, colors): + PolyCollection.set_facecolor(self, colors) + self._facecolors3d = PolyCollection.get_facecolor(self) + set_facecolors = set_facecolor + + def set_edgecolor(self, colors): + PolyCollection.set_edgecolor(self, colors) + self._edgecolors3d = PolyCollection.get_edgecolor(self) + set_edgecolors = set_edgecolor + + def set_alpha(self, alpha): + """ + Set the alpha tranparencies of the collection. *alpha* must be + a float or *None*. + + ACCEPTS: float or None + """ + if alpha is not None: + try: + float(alpha) + except TypeError: + raise TypeError('alpha must be a float or None') + artist.Artist.set_alpha(self, alpha) + try: + self._facecolors = mcolors.to_rgba_array( + self._facecolors3d, self._alpha) + except (AttributeError, TypeError, IndexError): + pass + try: + self._edgecolors = mcolors.to_rgba_array( + self._edgecolors3d, self._alpha) + except (AttributeError, TypeError, IndexError): + pass + self.stale = True + + def get_facecolors(self): + return self._facecolors2d + get_facecolor = get_facecolors + + def get_edgecolors(self): + return self._edgecolors2d + get_edgecolor = get_edgecolors + + def draw(self, renderer): + return Collection.draw(self, renderer) + + +def poly_collection_2d_to_3d(col, zs=0, zdir='z'): + """Convert a PolyCollection to a Poly3DCollection object.""" + segments_3d, codes = paths_to_3d_segments_with_codes(col.get_paths(), + zs, zdir) + col.__class__ = Poly3DCollection + col.set_verts_and_codes(segments_3d, codes) + col.set_3d_properties() + + +def juggle_axes(xs, ys, zs, zdir): + """ + Reorder coordinates so that 2D xs, ys can be plotted in the plane + orthogonal to zdir. zdir is normally x, y or z. However, if zdir + starts with a '-' it is interpreted as a compensation for rotate_axes. + """ + if zdir == 'x': + return zs, xs, ys + elif zdir == 'y': + return xs, zs, ys + elif zdir[0] == '-': + return rotate_axes(xs, ys, zs, zdir) + else: + return xs, ys, zs + + +def rotate_axes(xs, ys, zs, zdir): + """ + Reorder coordinates so that the axes are rotated with zdir along + the original z axis. Prepending the axis with a '-' does the + inverse transform, so zdir can be x, -x, y, -y, z or -z + """ + if zdir == 'x': + return ys, zs, xs + elif zdir == '-x': + return zs, xs, ys + + elif zdir == 'y': + return zs, xs, ys + elif zdir == '-y': + return ys, zs, xs + + else: + return xs, ys, zs + + +def get_colors(c, num): + """Stretch the color argument to provide the required number num""" + return _backports.broadcast_to( + mcolors.to_rgba_array(c) if len(c) else [0, 0, 0, 0], + (num, 4)) + + +def zalpha(colors, zs): + """Modify the alphas of the color list according to depth""" + # FIXME: This only works well if the points for *zs* are well-spaced + # in all three dimensions. Otherwise, at certain orientations, + # the min and max zs are very close together. + # Should really normalize against the viewing depth. + colors = get_colors(colors, len(zs)) + if len(zs): + norm = Normalize(min(zs), max(zs)) + sats = 1 - norm(zs) * 0.7 + colors = [(c[0], c[1], c[2], c[3] * s) for c, s in zip(colors, sats)] + return colors diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/axes3d.py b/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/axes3d.py new file mode 100644 index 0000000000..b99a090c62 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/axes3d.py @@ -0,0 +1,2958 @@ +""" +axes3d.py, original mplot3d version by John Porter +Created: 23 Sep 2005 + +Parts fixed by Reinier Heeres <reinier@heeres.eu> +Minor additions by Ben Axelrod <baxelrod@coroware.com> +Significant updates and revisions by Ben Root <ben.v.root@gmail.com> + +Module containing Axes3D, an object which can plot 3D objects on a +2D matplotlib figure. +""" +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six +from six.moves import map, xrange, zip, reduce + +import math +import warnings +from collections import defaultdict + +import numpy as np + +import matplotlib.axes as maxes +import matplotlib.cbook as cbook +import matplotlib.collections as mcoll +import matplotlib.colors as mcolors +import matplotlib.docstring as docstring +import matplotlib.scale as mscale +import matplotlib.transforms as mtransforms +from matplotlib.axes import Axes, rcParams +from matplotlib.cbook import _backports +from matplotlib.colors import Normalize, LightSource +from matplotlib.transforms import Bbox +from matplotlib.tri.triangulation import Triangulation + +from . import art3d +from . import proj3d +from . import axis3d + + +def unit_bbox(): + box = Bbox(np.array([[0, 0], [1, 1]])) + return box + + +class Axes3D(Axes): + """ + 3D axes object. + """ + name = '3d' + _shared_z_axes = cbook.Grouper() + + def __init__(self, fig, rect=None, *args, **kwargs): + ''' + Build an :class:`Axes3D` instance in + :class:`~matplotlib.figure.Figure` *fig* with + *rect=[left, bottom, width, height]* in + :class:`~matplotlib.figure.Figure` coordinates + + Optional keyword arguments: + + ================ ========================================= + Keyword Description + ================ ========================================= + *azim* Azimuthal viewing angle (default -60) + *elev* Elevation viewing angle (default 30) + *zscale* [%(scale)s] + *sharez* Other axes to share z-limits with + *proj_type* 'persp' or 'ortho' (default 'persp') + ================ ========================================= + + .. versionadded :: 1.2.1 + *sharez* + + ''' % {'scale': ' | '.join([repr(x) for x in mscale.get_scale_names()])} + + if rect is None: + rect = [0.0, 0.0, 1.0, 1.0] + self._cids = [] + + self.initial_azim = kwargs.pop('azim', -60) + self.initial_elev = kwargs.pop('elev', 30) + zscale = kwargs.pop('zscale', None) + sharez = kwargs.pop('sharez', None) + self.set_proj_type(kwargs.pop('proj_type', 'persp')) + + self.xy_viewLim = unit_bbox() + self.zz_viewLim = unit_bbox() + self.xy_dataLim = unit_bbox() + self.zz_dataLim = unit_bbox() + # inihibit autoscale_view until the axes are defined + # they can't be defined until Axes.__init__ has been called + self.view_init(self.initial_elev, self.initial_azim) + self._ready = 0 + + self._sharez = sharez + if sharez is not None: + self._shared_z_axes.join(self, sharez) + self._adjustable = 'datalim' + + super(Axes3D, self).__init__(fig, rect, + frameon=True, + *args, **kwargs) + # Disable drawing of axes by base class + super(Axes3D, self).set_axis_off() + # Enable drawing of axes by Axes3D class + self.set_axis_on() + self.M = None + + # func used to format z -- fall back on major formatters + self.fmt_zdata = None + + if zscale is not None: + self.set_zscale(zscale) + + if self.zaxis is not None: + self._zcid = self.zaxis.callbacks.connect( + 'units finalize', lambda: self._on_units_changed(scalez=True)) + else: + self._zcid = None + + self._ready = 1 + self.mouse_init() + self.set_top_view() + + self.patch.set_linewidth(0) + # Calculate the pseudo-data width and height + pseudo_bbox = self.transLimits.inverted().transform([(0, 0), (1, 1)]) + self._pseudo_w, self._pseudo_h = pseudo_bbox[1] - pseudo_bbox[0] + + self.figure.add_axes(self) + + def set_axis_off(self): + self._axis3don = False + self.stale = True + + def set_axis_on(self): + self._axis3don = True + self.stale = True + + def have_units(self): + """ + Return *True* if units are set on the *x*, *y*, or *z* axes + + """ + return (self.xaxis.have_units() or self.yaxis.have_units() or + self.zaxis.have_units()) + + def convert_zunits(self, z): + """ + For artists in an axes, if the zaxis has units support, + convert *z* using zaxis unit type + + .. versionadded :: 1.2.1 + + """ + return self.zaxis.convert_units(z) + + def _process_unit_info(self, xdata=None, ydata=None, zdata=None, + kwargs=None): + """ + Look for unit *kwargs* and update the axis instances as necessary + + """ + super(Axes3D, self)._process_unit_info(xdata=xdata, ydata=ydata, + kwargs=kwargs) + + if self.xaxis is None or self.yaxis is None or self.zaxis is None: + return + + if zdata is not None: + # we only need to update if there is nothing set yet. + if not self.zaxis.have_units(): + self.zaxis.update_units(xdata) + + # process kwargs 2nd since these will override default units + if kwargs is not None: + zunits = kwargs.pop('zunits', self.zaxis.units) + if zunits != self.zaxis.units: + self.zaxis.set_units(zunits) + # If the units being set imply a different converter, + # we need to update. + if zdata is not None: + self.zaxis.update_units(zdata) + + def set_top_view(self): + # this happens to be the right view for the viewing coordinates + # moved up and to the left slightly to fit labels and axes + xdwl = (0.95/self.dist) + xdw = (0.9/self.dist) + ydwl = (0.95/self.dist) + ydw = (0.9/self.dist) + + # This is purposely using the 2D Axes's set_xlim and set_ylim, + # because we are trying to place our viewing pane. + super(Axes3D, self).set_xlim(-xdwl, xdw, auto=None) + super(Axes3D, self).set_ylim(-ydwl, ydw, auto=None) + + def _init_axis(self): + '''Init 3D axes; overrides creation of regular X/Y axes''' + self.w_xaxis = axis3d.XAxis('x', self.xy_viewLim.intervalx, + self.xy_dataLim.intervalx, self) + self.xaxis = self.w_xaxis + self.w_yaxis = axis3d.YAxis('y', self.xy_viewLim.intervaly, + self.xy_dataLim.intervaly, self) + self.yaxis = self.w_yaxis + self.w_zaxis = axis3d.ZAxis('z', self.zz_viewLim.intervalx, + self.zz_dataLim.intervalx, self) + self.zaxis = self.w_zaxis + + for ax in self.xaxis, self.yaxis, self.zaxis: + ax.init3d() + + def get_children(self): + return [self.zaxis, ] + super(Axes3D, self).get_children() + + def _get_axis_list(self): + return super(Axes3D, self)._get_axis_list() + (self.zaxis, ) + + def unit_cube(self, vals=None): + minx, maxx, miny, maxy, minz, maxz = vals or self.get_w_lims() + xs, ys, zs = ([minx, maxx, maxx, minx, minx, maxx, maxx, minx], + [miny, miny, maxy, maxy, miny, miny, maxy, maxy], + [minz, minz, minz, minz, maxz, maxz, maxz, maxz]) + return list(zip(xs, ys, zs)) + + def tunit_cube(self, vals=None, M=None): + if M is None: + M = self.M + xyzs = self.unit_cube(vals) + tcube = proj3d.proj_points(xyzs, M) + return tcube + + def tunit_edges(self, vals=None, M=None): + tc = self.tunit_cube(vals, M) + edges = [(tc[0], tc[1]), + (tc[1], tc[2]), + (tc[2], tc[3]), + (tc[3], tc[0]), + + (tc[0], tc[4]), + (tc[1], tc[5]), + (tc[2], tc[6]), + (tc[3], tc[7]), + + (tc[4], tc[5]), + (tc[5], tc[6]), + (tc[6], tc[7]), + (tc[7], tc[4])] + return edges + + def draw(self, renderer): + # draw the background patch + self.patch.draw(renderer) + self._frameon = False + + # first, set the aspect + # this is duplicated from `axes._base._AxesBase.draw` + # but must be called before any of the artist are drawn as + # it adjusts the view limits and the size of the bounding box + # of the axes + locator = self.get_axes_locator() + if locator: + pos = locator(self, renderer) + self.apply_aspect(pos) + else: + self.apply_aspect() + + # add the projection matrix to the renderer + self.M = self.get_proj() + renderer.M = self.M + renderer.vvec = self.vvec + renderer.eye = self.eye + renderer.get_axis_position = self.get_axis_position + + # Calculate projection of collections and zorder them + for i, col in enumerate( + sorted(self.collections, + key=lambda col: col.do_3d_projection(renderer), + reverse=True)): + col.zorder = i + + # Calculate projection of patches and zorder them + for i, patch in enumerate( + sorted(self.patches, + key=lambda patch: patch.do_3d_projection(renderer), + reverse=True)): + patch.zorder = i + + if self._axis3don: + axes = (self.xaxis, self.yaxis, self.zaxis) + # Draw panes first + for ax in axes: + ax.draw_pane(renderer) + # Then axes + for ax in axes: + ax.draw(renderer) + + # Then rest + super(Axes3D, self).draw(renderer) + + def get_axis_position(self): + vals = self.get_w_lims() + tc = self.tunit_cube(vals, self.M) + xhigh = tc[1][2] > tc[2][2] + yhigh = tc[3][2] > tc[2][2] + zhigh = tc[0][2] > tc[2][2] + return xhigh, yhigh, zhigh + + def _on_units_changed(self, scalex=False, scaley=False, scalez=False): + """ + Callback for processing changes to axis units. + + Currently forces updates of data limits and view limits. + """ + self.relim() + self.autoscale_view(scalex=scalex, scaley=scaley, scalez=scalez) + + def update_datalim(self, xys, **kwargs): + pass + + def get_autoscale_on(self): + """ + Get whether autoscaling is applied for all axes on plot commands + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + return super(Axes3D, self).get_autoscale_on() and self.get_autoscalez_on() + + def get_autoscalez_on(self): + """ + Get whether autoscaling for the z-axis is applied on plot commands + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + return self._autoscaleZon + + def set_autoscale_on(self, b): + """ + Set whether autoscaling is applied on plot commands + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + + Parameters + ---------- + b : bool + .. ACCEPTS: bool + """ + super(Axes3D, self).set_autoscale_on(b) + self.set_autoscalez_on(b) + + def set_autoscalez_on(self, b): + """ + Set whether autoscaling for the z-axis is applied on plot commands + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + + Parameters + ---------- + b : bool + .. ACCEPTS: bool + """ + self._autoscaleZon = b + + def set_zmargin(self, m): + """ + Set padding of Z data limits prior to autoscaling. + + *m* times the data interval will be added to each + end of that interval before it is used in autoscaling. + + accepts: float in range 0 to 1 + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + if m < 0 or m > 1 : + raise ValueError("margin must be in range 0 to 1") + self._zmargin = m + self.stale = True + + def margins(self, *args, **kw): + """ + Convenience method to set or retrieve autoscaling margins. + + signatures:: + margins() + + returns xmargin, ymargin, zmargin + + :: + + margins(margin) + + margins(xmargin, ymargin, zmargin) + + margins(x=xmargin, y=ymargin, z=zmargin) + + margins(..., tight=False) + + All forms above set the xmargin, ymargin and zmargin + parameters. All keyword parameters are optional. A single argument + specifies xmargin, ymargin and zmargin. The *tight* parameter + is passed to :meth:`autoscale_view`, which is executed after + a margin is changed; the default here is *True*, on the + assumption that when margins are specified, no additional + padding to match tick marks is usually desired. Setting + *tight* to *None* will preserve the previous setting. + + Specifying any margin changes only the autoscaling; for example, + if *xmargin* is not None, then *xmargin* times the X data + interval will be added to each end of that interval before + it is used in autoscaling. + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + if not args and not kw: + return self._xmargin, self._ymargin, self._zmargin + + tight = kw.pop('tight', True) + mx = kw.pop('x', None) + my = kw.pop('y', None) + mz = kw.pop('z', None) + if not args: + pass + elif len(args) == 1: + mx = my = mz = args[0] + elif len(args) == 2: + warnings.warn( + "Passing exactly two positional arguments to Axes3D.margins " + "is deprecated. If needed, pass them as keyword arguments " + "instead", cbook.mplDeprecation) + mx, my = args + elif len(args) == 3: + mx, my, mz = args + else: + raise ValueError( + "Axes3D.margins takes at most three positional arguments") + if mx is not None: + self.set_xmargin(mx) + if my is not None: + self.set_ymargin(my) + if mz is not None: + self.set_zmargin(mz) + + scalex = mx is not None + scaley = my is not None + scalez = mz is not None + + self.autoscale_view(tight=tight, scalex=scalex, scaley=scaley, + scalez=scalez) + + def autoscale(self, enable=True, axis='both', tight=None): + """ + Convenience method for simple axis view autoscaling. + See :meth:`matplotlib.axes.Axes.autoscale` for full explanation. + Note that this function behaves the same, but for all + three axes. Therefore, 'z' can be passed for *axis*, + and 'both' applies to all three axes. + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + if enable is None: + scalex = True + scaley = True + scalez = True + else: + if axis in ['x', 'both']: + self._autoscaleXon = scalex = bool(enable) + else: + scalex = False + if axis in ['y', 'both']: + self._autoscaleYon = scaley = bool(enable) + else: + scaley = False + if axis in ['z', 'both']: + self._autoscaleZon = scalez = bool(enable) + else: + scalez = False + self.autoscale_view(tight=tight, scalex=scalex, scaley=scaley, + scalez=scalez) + + def auto_scale_xyz(self, X, Y, Z=None, had_data=None): + x, y, z = map(np.asarray, (X, Y, Z)) + try: + x, y = x.flatten(), y.flatten() + if Z is not None: + z = z.flatten() + except AttributeError: + raise + + # This updates the bounding boxes as to keep a record as + # to what the minimum sized rectangular volume holds the + # data. + self.xy_dataLim.update_from_data_xy(np.array([x, y]).T, not had_data) + if z is not None: + self.zz_dataLim.update_from_data_xy(np.array([z, z]).T, not had_data) + + # Let autoscale_view figure out how to use this data. + self.autoscale_view() + + def autoscale_view(self, tight=None, scalex=True, scaley=True, + scalez=True): + """ + Autoscale the view limits using the data limits. + See :meth:`matplotlib.axes.Axes.autoscale_view` for documentation. + Note that this function applies to the 3D axes, and as such + adds the *scalez* to the function arguments. + + .. versionchanged :: 1.1.0 + Function signature was changed to better match the 2D version. + *tight* is now explicitly a kwarg and placed first. + + .. versionchanged :: 1.2.1 + This is now fully functional. + + """ + if not self._ready: + return + + # This method looks at the rectangular volume (see above) + # of data and decides how to scale the view portal to fit it. + if tight is None: + # if image data only just use the datalim + _tight = self._tight or (len(self.images)>0 and + len(self.lines)==0 and + len(self.patches)==0) + else: + _tight = self._tight = bool(tight) + + if scalex and self._autoscaleXon: + xshared = self._shared_x_axes.get_siblings(self) + dl = [ax.dataLim for ax in xshared] + bb = mtransforms.BboxBase.union(dl) + x0, x1 = self.xy_dataLim.intervalx + xlocator = self.xaxis.get_major_locator() + try: + x0, x1 = xlocator.nonsingular(x0, x1) + except AttributeError: + x0, x1 = mtransforms.nonsingular(x0, x1, increasing=False, + expander=0.05) + if self._xmargin > 0: + delta = (x1 - x0) * self._xmargin + x0 -= delta + x1 += delta + if not _tight: + x0, x1 = xlocator.view_limits(x0, x1) + self.set_xbound(x0, x1) + + if scaley and self._autoscaleYon: + yshared = self._shared_y_axes.get_siblings(self) + dl = [ax.dataLim for ax in yshared] + bb = mtransforms.BboxBase.union(dl) + y0, y1 = self.xy_dataLim.intervaly + ylocator = self.yaxis.get_major_locator() + try: + y0, y1 = ylocator.nonsingular(y0, y1) + except AttributeError: + y0, y1 = mtransforms.nonsingular(y0, y1, increasing=False, + expander=0.05) + if self._ymargin > 0: + delta = (y1 - y0) * self._ymargin + y0 -= delta + y1 += delta + if not _tight: + y0, y1 = ylocator.view_limits(y0, y1) + self.set_ybound(y0, y1) + + if scalez and self._autoscaleZon: + zshared = self._shared_z_axes.get_siblings(self) + dl = [ax.dataLim for ax in zshared] + bb = mtransforms.BboxBase.union(dl) + z0, z1 = self.zz_dataLim.intervalx + zlocator = self.zaxis.get_major_locator() + try: + z0, z1 = zlocator.nonsingular(z0, z1) + except AttributeError: + z0, z1 = mtransforms.nonsingular(z0, z1, increasing=False, + expander=0.05) + if self._zmargin > 0: + delta = (z1 - z0) * self._zmargin + z0 -= delta + z1 += delta + if not _tight: + z0, z1 = zlocator.view_limits(z0, z1) + self.set_zbound(z0, z1) + + def get_w_lims(self): + '''Get 3D world limits.''' + minx, maxx = self.get_xlim3d() + miny, maxy = self.get_ylim3d() + minz, maxz = self.get_zlim3d() + return minx, maxx, miny, maxy, minz, maxz + + def _determine_lims(self, xmin=None, xmax=None, *args, **kwargs): + if xmax is None and cbook.iterable(xmin): + xmin, xmax = xmin + if xmin == xmax: + xmin -= 0.05 + xmax += 0.05 + return (xmin, xmax) + + def set_xlim3d(self, left=None, right=None, emit=True, auto=False, **kw): + """ + Set 3D x limits. + + See :meth:`matplotlib.axes.Axes.set_xlim` for full documentation. + + """ + if 'xmin' in kw: + left = kw.pop('xmin') + if 'xmax' in kw: + right = kw.pop('xmax') + if kw: + raise ValueError("unrecognized kwargs: %s" % list(kw)) + + if right is None and cbook.iterable(left): + left, right = left + + self._process_unit_info(xdata=(left, right)) + left = self._validate_converted_limits(left, self.convert_xunits) + right = self._validate_converted_limits(right, self.convert_xunits) + + old_left, old_right = self.get_xlim() + if left is None: + left = old_left + if right is None: + right = old_right + + if left == right: + warnings.warn(('Attempting to set identical left==right results\n' + 'in singular transformations; automatically expanding.\n' + 'left=%s, right=%s') % (left, right)) + left, right = mtransforms.nonsingular(left, right, increasing=False) + left, right = self.xaxis.limit_range_for_scale(left, right) + self.xy_viewLim.intervalx = (left, right) + + if auto is not None: + self._autoscaleXon = bool(auto) + + if emit: + self.callbacks.process('xlim_changed', self) + # Call all of the other x-axes that are shared with this one + for other in self._shared_x_axes.get_siblings(self): + if other is not self: + other.set_xlim(self.xy_viewLim.intervalx, + emit=False, auto=auto) + if (other.figure != self.figure and + other.figure.canvas is not None): + other.figure.canvas.draw_idle() + self.stale = True + return left, right + set_xlim = set_xlim3d + + def set_ylim3d(self, bottom=None, top=None, emit=True, auto=False, **kw): + """ + Set 3D y limits. + + See :meth:`matplotlib.axes.Axes.set_ylim` for full documentation. + + """ + if 'ymin' in kw: + bottom = kw.pop('ymin') + if 'ymax' in kw: + top = kw.pop('ymax') + if kw: + raise ValueError("unrecognized kwargs: %s" % list(kw)) + + if top is None and cbook.iterable(bottom): + bottom, top = bottom + + self._process_unit_info(ydata=(bottom, top)) + bottom = self._validate_converted_limits(bottom, self.convert_yunits) + top = self._validate_converted_limits(top, self.convert_yunits) + + old_bottom, old_top = self.get_ylim() + if bottom is None: + bottom = old_bottom + if top is None: + top = old_top + + if top == bottom: + warnings.warn(('Attempting to set identical bottom==top results\n' + 'in singular transformations; automatically expanding.\n' + 'bottom=%s, top=%s') % (bottom, top)) + bottom, top = mtransforms.nonsingular(bottom, top, increasing=False) + bottom, top = self.yaxis.limit_range_for_scale(bottom, top) + self.xy_viewLim.intervaly = (bottom, top) + + if auto is not None: + self._autoscaleYon = bool(auto) + + if emit: + self.callbacks.process('ylim_changed', self) + # Call all of the other y-axes that are shared with this one + for other in self._shared_y_axes.get_siblings(self): + if other is not self: + other.set_ylim(self.xy_viewLim.intervaly, + emit=False, auto=auto) + if (other.figure != self.figure and + other.figure.canvas is not None): + other.figure.canvas.draw_idle() + self.stale = True + return bottom, top + set_ylim = set_ylim3d + + def set_zlim3d(self, bottom=None, top=None, emit=True, auto=False, **kw): + """ + Set 3D z limits. + + See :meth:`matplotlib.axes.Axes.set_ylim` for full documentation + + """ + if 'zmin' in kw: + bottom = kw.pop('zmin') + if 'zmax' in kw: + top = kw.pop('zmax') + if kw: + raise ValueError("unrecognized kwargs: %s" % list(kw)) + + if top is None and cbook.iterable(bottom): + bottom, top = bottom + + self._process_unit_info(zdata=(bottom, top)) + bottom = self._validate_converted_limits(bottom, self.convert_zunits) + top = self._validate_converted_limits(top, self.convert_zunits) + + old_bottom, old_top = self.get_zlim() + if bottom is None: + bottom = old_bottom + if top is None: + top = old_top + + if top == bottom: + warnings.warn(('Attempting to set identical bottom==top results\n' + 'in singular transformations; automatically expanding.\n' + 'bottom=%s, top=%s') % (bottom, top)) + bottom, top = mtransforms.nonsingular(bottom, top, increasing=False) + bottom, top = self.zaxis.limit_range_for_scale(bottom, top) + self.zz_viewLim.intervalx = (bottom, top) + + if auto is not None: + self._autoscaleZon = bool(auto) + + if emit: + self.callbacks.process('zlim_changed', self) + # Call all of the other y-axes that are shared with this one + for other in self._shared_z_axes.get_siblings(self): + if other is not self: + other.set_zlim(self.zz_viewLim.intervalx, + emit=False, auto=auto) + if (other.figure != self.figure and + other.figure.canvas is not None): + other.figure.canvas.draw_idle() + self.stale = True + return bottom, top + set_zlim = set_zlim3d + + def get_xlim3d(self): + return tuple(self.xy_viewLim.intervalx) + get_xlim3d.__doc__ = maxes.Axes.get_xlim.__doc__ + get_xlim = get_xlim3d + if get_xlim.__doc__ is not None: + get_xlim.__doc__ += """ + .. versionchanged :: 1.1.0 + This function now correctly refers to the 3D x-limits + """ + + def get_ylim3d(self): + return tuple(self.xy_viewLim.intervaly) + get_ylim3d.__doc__ = maxes.Axes.get_ylim.__doc__ + get_ylim = get_ylim3d + if get_ylim.__doc__ is not None: + get_ylim.__doc__ += """ + .. versionchanged :: 1.1.0 + This function now correctly refers to the 3D y-limits. + """ + + def get_zlim3d(self): + '''Get 3D z limits.''' + return tuple(self.zz_viewLim.intervalx) + get_zlim = get_zlim3d + + def get_zscale(self): + """ + Return the zaxis scale string %s + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ % (", ".join(mscale.get_scale_names())) + return self.zaxis.get_scale() + + # We need to slightly redefine these to pass scalez=False + # to their calls of autoscale_view. + def set_xscale(self, value, **kwargs): + self.xaxis._set_scale(value, **kwargs) + self.autoscale_view(scaley=False, scalez=False) + self._update_transScale() + if maxes.Axes.set_xscale.__doc__ is not None: + set_xscale.__doc__ = maxes.Axes.set_xscale.__doc__ + """ + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + + def set_yscale(self, value, **kwargs): + self.yaxis._set_scale(value, **kwargs) + self.autoscale_view(scalex=False, scalez=False) + self._update_transScale() + self.stale = True + if maxes.Axes.set_yscale.__doc__ is not None: + set_yscale.__doc__ = maxes.Axes.set_yscale.__doc__ + """ + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + + @docstring.dedent_interpd + def set_zscale(self, value, **kwargs): + """ + Set the scaling of the z-axis: %(scale)s + + ACCEPTS: [%(scale)s] + + Different kwargs are accepted, depending on the scale: + %(scale_docs)s + + .. note :: + Currently, Axes3D objects only supports linear scales. + Other scales may or may not work, and support for these + is improving with each release. + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + self.zaxis._set_scale(value, **kwargs) + self.autoscale_view(scalex=False, scaley=False) + self._update_transScale() + self.stale = True + + def set_zticks(self, *args, **kwargs): + """ + Set z-axis tick locations. + See :meth:`matplotlib.axes.Axes.set_yticks` for more details. + + .. note:: + Minor ticks are not supported. + + .. versionadded:: 1.1.0 + """ + return self.zaxis.set_ticks(*args, **kwargs) + + def get_zticks(self, minor=False): + """ + Return the z ticks as a list of locations + See :meth:`matplotlib.axes.Axes.get_yticks` for more details. + + .. note:: + Minor ticks are not supported. + + .. versionadded:: 1.1.0 + """ + return self.zaxis.get_ticklocs(minor=minor) + + def get_zmajorticklabels(self): + """ + Get the ztick labels as a list of Text instances + + .. versionadded :: 1.1.0 + """ + return cbook.silent_list('Text zticklabel', + self.zaxis.get_majorticklabels()) + + def get_zminorticklabels(self): + """ + Get the ztick labels as a list of Text instances + + .. note:: + Minor ticks are not supported. This function was added + only for completeness. + + .. versionadded :: 1.1.0 + """ + return cbook.silent_list('Text zticklabel', + self.zaxis.get_minorticklabels()) + + def set_zticklabels(self, *args, **kwargs): + """ + Set z-axis tick labels. + See :meth:`matplotlib.axes.Axes.set_yticklabels` for more details. + + .. note:: + Minor ticks are not supported by Axes3D objects. + + .. versionadded:: 1.1.0 + """ + return self.zaxis.set_ticklabels(*args, **kwargs) + + def get_zticklabels(self, minor=False): + """ + Get ztick labels as a list of Text instances. + See :meth:`matplotlib.axes.Axes.get_yticklabels` for more details. + + .. note:: + Minor ticks are not supported. + + .. versionadded:: 1.1.0 + """ + return cbook.silent_list('Text zticklabel', + self.zaxis.get_ticklabels(minor=minor)) + + def zaxis_date(self, tz=None): + """ + Sets up z-axis ticks and labels that treat the z data as dates. + + *tz* is a timezone string or :class:`tzinfo` instance. + Defaults to rc value. + + .. note:: + This function is merely provided for completeness. + Axes3D objects do not officially support dates for ticks, + and so this may or may not work as expected. + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + self.zaxis.axis_date(tz) + + def get_zticklines(self): + """ + Get ztick lines as a list of Line2D instances. + Note that this function is provided merely for completeness. + These lines are re-calculated as the display changes. + + .. versionadded:: 1.1.0 + """ + return self.zaxis.get_ticklines() + + def clabel(self, *args, **kwargs): + """ + This function is currently not implemented for 3D axes. + Returns *None*. + """ + return None + + def view_init(self, elev=None, azim=None): + """ + Set the elevation and azimuth of the axes. + + This can be used to rotate the axes programmatically. + + 'elev' stores the elevation angle in the z plane. + 'azim' stores the azimuth angle in the x,y plane. + + if elev or azim are None (default), then the initial value + is used which was specified in the :class:`Axes3D` constructor. + """ + + self.dist = 10 + + if elev is None: + self.elev = self.initial_elev + else: + self.elev = elev + + if azim is None: + self.azim = self.initial_azim + else: + self.azim = azim + + def set_proj_type(self, proj_type): + """ + Set the projection type. + + Parameters + ---------- + proj_type : str + Type of projection, accepts 'persp' and 'ortho'. + + """ + if proj_type == 'persp': + self._projection = proj3d.persp_transformation + elif proj_type == 'ortho': + self._projection = proj3d.ortho_transformation + else: + raise ValueError("unrecognized projection: %s" % proj_type) + + def get_proj(self): + """ + Create the projection matrix from the current viewing position. + + elev stores the elevation angle in the z plane + azim stores the azimuth angle in the x,y plane + + dist is the distance of the eye viewing point from the object + point. + + """ + relev, razim = np.pi * self.elev/180, np.pi * self.azim/180 + + xmin, xmax = self.get_xlim3d() + ymin, ymax = self.get_ylim3d() + zmin, zmax = self.get_zlim3d() + + # transform to uniform world coordinates 0-1.0,0-1.0,0-1.0 + worldM = proj3d.world_transformation(xmin, xmax, + ymin, ymax, + zmin, zmax) + + # look into the middle of the new coordinates + R = np.array([0.5, 0.5, 0.5]) + + xp = R[0] + np.cos(razim) * np.cos(relev) * self.dist + yp = R[1] + np.sin(razim) * np.cos(relev) * self.dist + zp = R[2] + np.sin(relev) * self.dist + E = np.array((xp, yp, zp)) + + self.eye = E + self.vvec = R - E + self.vvec = self.vvec / proj3d.mod(self.vvec) + + if abs(relev) > np.pi/2: + # upside down + V = np.array((0, 0, -1)) + else: + V = np.array((0, 0, 1)) + zfront, zback = -self.dist, self.dist + + viewM = proj3d.view_transformation(E, R, V) + projM = self._projection(zfront, zback) + M0 = np.dot(viewM, worldM) + M = np.dot(projM, M0) + return M + + def mouse_init(self, rotate_btn=1, zoom_btn=3): + """Initializes mouse button callbacks to enable 3D rotation of + the axes. Also optionally sets the mouse buttons for 3D rotation + and zooming. + + ============ ======================================================= + Argument Description + ============ ======================================================= + *rotate_btn* The integer or list of integers specifying which mouse + button or buttons to use for 3D rotation of the axes. + Default = 1. + + *zoom_btn* The integer or list of integers specifying which mouse + button or buttons to use to zoom the 3D axes. + Default = 3. + ============ ======================================================= + + """ + self.button_pressed = None + canv = self.figure.canvas + if canv is not None: + c1 = canv.mpl_connect('motion_notify_event', self._on_move) + c2 = canv.mpl_connect('button_press_event', self._button_press) + c3 = canv.mpl_connect('button_release_event', self._button_release) + self._cids = [c1, c2, c3] + else: + warnings.warn( + "Axes3D.figure.canvas is 'None', mouse rotation disabled. " + "Set canvas then call Axes3D.mouse_init().") + + # coerce scalars into array-like, then convert into + # a regular list to avoid comparisons against None + # which breaks in recent versions of numpy. + self._rotate_btn = np.atleast_1d(rotate_btn).tolist() + self._zoom_btn = np.atleast_1d(zoom_btn).tolist() + + def can_zoom(self): + """ + Return *True* if this axes supports the zoom box button functionality. + + 3D axes objects do not use the zoom box button. + """ + return False + + def can_pan(self): + """ + Return *True* if this axes supports the pan/zoom button functionality. + + 3D axes objects do not use the pan/zoom button. + """ + return False + + def cla(self): + """ + Clear axes + """ + # Disabling mouse interaction might have been needed a long + # time ago, but I can't find a reason for it now - BVR (2012-03) + #self.disable_mouse_rotation() + super(Axes3D, self).cla() + self.zaxis.cla() + + if self._sharez is not None: + self.zaxis.major = self._sharez.zaxis.major + self.zaxis.minor = self._sharez.zaxis.minor + z0, z1 = self._sharez.get_zlim() + self.set_zlim(z0, z1, emit=False, auto=None) + self.zaxis._set_scale(self._sharez.zaxis.get_scale()) + else: + self.zaxis._set_scale('linear') + try: + self.set_zlim(0, 1) + except TypeError: + pass + + self._autoscaleZon = True + self._zmargin = 0 + + self.grid(rcParams['axes3d.grid']) + + def disable_mouse_rotation(self): + """Disable mouse button callbacks. + """ + # Disconnect the various events we set. + for cid in self._cids: + self.figure.canvas.mpl_disconnect(cid) + + self._cids = [] + + def _button_press(self, event): + if event.inaxes == self: + self.button_pressed = event.button + self.sx, self.sy = event.xdata, event.ydata + + def _button_release(self, event): + self.button_pressed = None + + def format_zdata(self, z): + """ + Return *z* string formatted. This function will use the + :attr:`fmt_zdata` attribute if it is callable, else will fall + back on the zaxis major formatter + """ + try: return self.fmt_zdata(z) + except (AttributeError, TypeError): + func = self.zaxis.get_major_formatter().format_data_short + val = func(z) + return val + + def format_coord(self, xd, yd): + """ + Given the 2D view coordinates attempt to guess a 3D coordinate. + Looks for the nearest edge to the point and then assumes that + the point is at the same z location as the nearest point on the edge. + """ + + if self.M is None: + return '' + + if self.button_pressed in self._rotate_btn: + return 'azimuth=%d deg, elevation=%d deg ' % (self.azim, self.elev) + # ignore xd and yd and display angles instead + + # nearest edge + p0, p1 = min(self.tunit_edges(), + key=lambda edge: proj3d.line2d_seg_dist( + edge[0], edge[1], (xd, yd))) + + # scale the z value to match + x0, y0, z0 = p0 + x1, y1, z1 = p1 + d0 = np.hypot(x0-xd, y0-yd) + d1 = np.hypot(x1-xd, y1-yd) + dt = d0+d1 + z = d1/dt * z0 + d0/dt * z1 + + x, y, z = proj3d.inv_transform(xd, yd, z, self.M) + + xs = self.format_xdata(x) + ys = self.format_ydata(y) + zs = self.format_zdata(z) + return 'x=%s, y=%s, z=%s' % (xs, ys, zs) + + def _on_move(self, event): + """Mouse moving + + button-1 rotates by default. Can be set explicitly in mouse_init(). + button-3 zooms by default. Can be set explicitly in mouse_init(). + """ + + if not self.button_pressed: + return + + if self.M is None: + return + + x, y = event.xdata, event.ydata + # In case the mouse is out of bounds. + if x is None: + return + + dx, dy = x - self.sx, y - self.sy + w = self._pseudo_w + h = self._pseudo_h + self.sx, self.sy = x, y + + # Rotation + if self.button_pressed in self._rotate_btn: + # rotate viewing point + # get the x and y pixel coords + if dx == 0 and dy == 0: + return + self.elev = art3d.norm_angle(self.elev - (dy/h)*180) + self.azim = art3d.norm_angle(self.azim - (dx/w)*180) + self.get_proj() + self.stale = True + self.figure.canvas.draw_idle() + +# elif self.button_pressed == 2: + # pan view + # project xv,yv,zv -> xw,yw,zw + # pan +# pass + + # Zoom + elif self.button_pressed in self._zoom_btn: + # zoom view + # hmmm..this needs some help from clipping.... + minx, maxx, miny, maxy, minz, maxz = self.get_w_lims() + df = 1-((h - dy)/h) + dx = (maxx-minx)*df + dy = (maxy-miny)*df + dz = (maxz-minz)*df + self.set_xlim3d(minx - dx, maxx + dx) + self.set_ylim3d(miny - dy, maxy + dy) + self.set_zlim3d(minz - dz, maxz + dz) + self.get_proj() + self.figure.canvas.draw_idle() + + def set_zlabel(self, zlabel, fontdict=None, labelpad=None, **kwargs): + ''' + Set zlabel. See doc for :meth:`set_ylabel` for description. + + ''' + if labelpad is not None : self.zaxis.labelpad = labelpad + return self.zaxis.set_label_text(zlabel, fontdict, **kwargs) + + def get_zlabel(self): + """ + Get the z-label text string. + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + label = self.zaxis.get_label() + return label.get_text() + + #### Axes rectangle characteristics + + def get_frame_on(self): + """ + Get whether the 3D axes panels are drawn. + + .. versionadded :: 1.1.0 + """ + return self._frameon + + def set_frame_on(self, b): + """ + Set whether the 3D axes panels are drawn. + + .. versionadded :: 1.1.0 + + Parameters + ---------- + b : bool + .. ACCEPTS: bool + """ + self._frameon = bool(b) + self.stale = True + + def get_axisbelow(self): + """ + Get whether axis below is true or not. + + For axes3d objects, this will always be *True* + + .. versionadded :: 1.1.0 + This function was added for completeness. + """ + return True + + def set_axisbelow(self, b): + """ + Set whether axis ticks and gridlines are above or below most artists. + + For axes3d objects, this will ignore any settings and just use *True* + + .. versionadded :: 1.1.0 + This function was added for completeness. + + Parameters + ---------- + b : bool + .. ACCEPTS: bool + """ + self._axisbelow = True + self.stale = True + + def grid(self, b=True, **kwargs): + ''' + Set / unset 3D grid. + + .. note:: + + Currently, this function does not behave the same as + :meth:`matplotlib.axes.Axes.grid`, but it is intended to + eventually support that behavior. + + .. versionchanged :: 1.1.0 + This function was changed, but not tested. Please report any bugs. + ''' + # TODO: Operate on each axes separately + if len(kwargs): + b = True + self._draw_grid = cbook._string_to_bool(b) + self.stale = True + + def ticklabel_format(self, **kwargs): + """ + Convenience method for manipulating the ScalarFormatter + used by default for linear axes in Axed3D objects. + + See :meth:`matplotlib.axes.Axes.ticklabel_format` for full + documentation. Note that this version applies to all three + axes of the Axes3D object. Therefore, the *axis* argument + will also accept a value of 'z' and the value of 'both' will + apply to all three axes. + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + style = kwargs.pop('style', '').lower() + scilimits = kwargs.pop('scilimits', None) + useOffset = kwargs.pop('useOffset', None) + axis = kwargs.pop('axis', 'both').lower() + if scilimits is not None: + try: + m, n = scilimits + m+n+1 # check that both are numbers + except (ValueError, TypeError): + raise ValueError("scilimits must be a sequence of 2 integers") + if style[:3] == 'sci': + sb = True + elif style in ['plain', 'comma']: + sb = False + if style == 'plain': + cb = False + else: + cb = True + raise NotImplementedError("comma style remains to be added") + elif style == '': + sb = None + else: + raise ValueError("%s is not a valid style value") + try: + if sb is not None: + if axis in ['both', 'z']: + self.xaxis.major.formatter.set_scientific(sb) + if axis in ['both', 'y']: + self.yaxis.major.formatter.set_scientific(sb) + if axis in ['both', 'z'] : + self.zaxis.major.formatter.set_scientific(sb) + if scilimits is not None: + if axis in ['both', 'x']: + self.xaxis.major.formatter.set_powerlimits(scilimits) + if axis in ['both', 'y']: + self.yaxis.major.formatter.set_powerlimits(scilimits) + if axis in ['both', 'z']: + self.zaxis.major.formatter.set_powerlimits(scilimits) + if useOffset is not None: + if axis in ['both', 'x']: + self.xaxis.major.formatter.set_useOffset(useOffset) + if axis in ['both', 'y']: + self.yaxis.major.formatter.set_useOffset(useOffset) + if axis in ['both', 'z']: + self.zaxis.major.formatter.set_useOffset(useOffset) + except AttributeError: + raise AttributeError( + "This method only works with the ScalarFormatter.") + + def locator_params(self, axis='both', tight=None, **kwargs): + """ + Convenience method for controlling tick locators. + + See :meth:`matplotlib.axes.Axes.locator_params` for full + documentation Note that this is for Axes3D objects, + therefore, setting *axis* to 'both' will result in the + parameters being set for all three axes. Also, *axis* + can also take a value of 'z' to apply parameters to the + z axis. + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + _x = axis in ['x', 'both'] + _y = axis in ['y', 'both'] + _z = axis in ['z', 'both'] + if _x: + self.xaxis.get_major_locator().set_params(**kwargs) + if _y: + self.yaxis.get_major_locator().set_params(**kwargs) + if _z: + self.zaxis.get_major_locator().set_params(**kwargs) + self.autoscale_view(tight=tight, scalex=_x, scaley=_y, scalez=_z) + + def tick_params(self, axis='both', **kwargs): + """ + Convenience method for changing the appearance of ticks and + tick labels. + + See :meth:`matplotlib.axes.Axes.tick_params` for more complete + documentation. + + The only difference is that setting *axis* to 'both' will + mean that the settings are applied to all three axes. Also, + the *axis* parameter also accepts a value of 'z', which + would mean to apply to only the z-axis. + + Also, because of how Axes3D objects are drawn very differently + from regular 2D axes, some of these settings may have + ambiguous meaning. For simplicity, the 'z' axis will + accept settings as if it was like the 'y' axis. + + .. note:: + While this function is currently implemented, the core part + of the Axes3D object may ignore some of these settings. + Future releases will fix this. Priority will be given to + those who file bugs. + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + super(Axes3D, self).tick_params(axis, **kwargs) + if axis in ['z', 'both'] : + zkw = dict(kwargs) + zkw.pop('top', None) + zkw.pop('bottom', None) + zkw.pop('labeltop', None) + zkw.pop('labelbottom', None) + self.zaxis.set_tick_params(**zkw) + + ### data limits, ticks, tick labels, and formatting + + def invert_zaxis(self): + """ + Invert the z-axis. + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + bottom, top = self.get_zlim() + self.set_zlim(top, bottom, auto=None) + + def zaxis_inverted(self): + ''' + Returns True if the z-axis is inverted. + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + ''' + bottom, top = self.get_zlim() + return top < bottom + + def get_zbound(self): + """ + Returns the z-axis numerical bounds where:: + + lowerBound < upperBound + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + bottom, top = self.get_zlim() + if bottom < top: + return bottom, top + else: + return top, bottom + + def set_zbound(self, lower=None, upper=None): + """ + Set the lower and upper numerical bounds of the z-axis. + This method will honor axes inversion regardless of parameter order. + It will not change the :attr:`_autoscaleZon` attribute. + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + if upper is None and cbook.iterable(lower): + lower,upper = lower + + old_lower,old_upper = self.get_zbound() + + if lower is None: lower = old_lower + if upper is None: upper = old_upper + + if self.zaxis_inverted(): + if lower < upper: + self.set_zlim(upper, lower, auto=None) + else: + self.set_zlim(lower, upper, auto=None) + else : + if lower < upper: + self.set_zlim(lower, upper, auto=None) + else : + self.set_zlim(upper, lower, auto=None) + + def text(self, x, y, z, s, zdir=None, **kwargs): + ''' + Add text to the plot. kwargs will be passed on to Axes.text, + except for the `zdir` keyword, which sets the direction to be + used as the z direction. + ''' + text = super(Axes3D, self).text(x, y, s, **kwargs) + art3d.text_2d_to_3d(text, z, zdir) + return text + + text3D = text + text2D = Axes.text + + def plot(self, xs, ys, *args, **kwargs): + ''' + Plot 2D or 3D data. + + ========== ================================================ + Argument Description + ========== ================================================ + *xs*, *ys* x, y coordinates of vertices + + *zs* z value(s), either one for all points or one for + each point. + *zdir* Which direction to use as z ('x', 'y' or 'z') + when plotting a 2D set. + ========== ================================================ + + Other arguments are passed on to + :func:`~matplotlib.axes.Axes.plot` + ''' + had_data = self.has_data() + + # `zs` can be passed positionally or as keyword; checking whether + # args[0] is a string matches the behavior of 2D `plot` (via + # `_process_plot_var_args`). + if args and not isinstance(args[0], six.string_types): + zs = args[0] + args = args[1:] + if 'zs' in kwargs: + raise TypeError("plot() for multiple values for argument 'z'") + else: + zs = kwargs.pop('zs', 0) + zdir = kwargs.pop('zdir', 'z') + + # Match length + zs = _backports.broadcast_to(zs, len(xs)) + + lines = super(Axes3D, self).plot(xs, ys, *args, **kwargs) + for line in lines: + art3d.line_2d_to_3d(line, zs=zs, zdir=zdir) + + xs, ys, zs = art3d.juggle_axes(xs, ys, zs, zdir) + self.auto_scale_xyz(xs, ys, zs, had_data) + return lines + + plot3D = plot + + def plot_surface(self, X, Y, Z, *args, **kwargs): + """ + Create a surface plot. + + By default it will be colored in shades of a solid color, but it also + supports color mapping by supplying the *cmap* argument. + + .. note:: + + The *rcount* and *ccount* kwargs, which both default to 50, + determine the maximum number of samples used in each direction. If + the input data is larger, it will be downsampled (by slicing) to + these numbers of points. + + Parameters + ---------- + X, Y, Z : 2d arrays + Data values. + + rcount, ccount : int + Maximum number of samples used in each direction. If the input + data is larger, it will be downsampled (by slicing) to these + numbers of points. Defaults to 50. + + .. versionadded:: 2.0 + + rstride, cstride : int + Downsampling stride in each direction. These arguments are + mutually exclusive with *rcount* and *ccount*. If only one of + *rstride* or *cstride* is set, the other defaults to 10. + + 'classic' mode uses a default of ``rstride = cstride = 10`` instead + of the new default of ``rcount = ccount = 50``. + + color : color-like + Color of the surface patches. + + cmap : Colormap + Colormap of the surface patches. + + facecolors : array-like of colors. + Colors of each individual patch. + + norm : Normalize + Normalization for the colormap. + + vmin, vmax : float + Bounds for the normalization. + + shade : bool + Whether to shade the face colors. + + **kwargs : + Other arguments are forwarded to `.Poly3DCollection`. + """ + + had_data = self.has_data() + + if Z.ndim != 2: + raise ValueError("Argument Z must be 2-dimensional.") + # TODO: Support masked arrays + X, Y, Z = np.broadcast_arrays(X, Y, Z) + rows, cols = Z.shape + + has_stride = 'rstride' in kwargs or 'cstride' in kwargs + has_count = 'rcount' in kwargs or 'ccount' in kwargs + + if has_stride and has_count: + raise ValueError("Cannot specify both stride and count arguments") + + rstride = kwargs.pop('rstride', 10) + cstride = kwargs.pop('cstride', 10) + rcount = kwargs.pop('rcount', 50) + ccount = kwargs.pop('ccount', 50) + + if rcParams['_internal.classic_mode']: + # Strides have priority over counts in classic mode. + # So, only compute strides from counts + # if counts were explicitly given + if has_count: + rstride = int(max(np.ceil(rows / rcount), 1)) + cstride = int(max(np.ceil(cols / ccount), 1)) + else: + # If the strides are provided then it has priority. + # Otherwise, compute the strides from the counts. + if not has_stride: + rstride = int(max(np.ceil(rows / rcount), 1)) + cstride = int(max(np.ceil(cols / ccount), 1)) + + if 'facecolors' in kwargs: + fcolors = kwargs.pop('facecolors') + else: + color = kwargs.pop('color', None) + if color is None: + color = self._get_lines.get_next_color() + color = np.array(mcolors.to_rgba(color)) + fcolors = None + + cmap = kwargs.get('cmap', None) + norm = kwargs.pop('norm', None) + vmin = kwargs.pop('vmin', None) + vmax = kwargs.pop('vmax', None) + linewidth = kwargs.get('linewidth', None) + shade = kwargs.pop('shade', cmap is None) + lightsource = kwargs.pop('lightsource', None) + + # Shade the data + if shade and cmap is not None and fcolors is not None: + fcolors = self._shade_colors_lightsource(Z, cmap, lightsource) + + polys = [] + # Only need these vectors to shade if there is no cmap + if cmap is None and shade : + totpts = int(np.ceil((rows - 1) / rstride) * + np.ceil((cols - 1) / cstride)) + v1 = np.empty((totpts, 3)) + v2 = np.empty((totpts, 3)) + # This indexes the vertex points + which_pt = 0 + + + #colset contains the data for coloring: either average z or the facecolor + colset = [] + for rs in xrange(0, rows-1, rstride): + for cs in xrange(0, cols-1, cstride): + ps = [] + for a in (X, Y, Z): + ztop = a[rs,cs:min(cols, cs+cstride+1)] + zleft = a[rs+1:min(rows, rs+rstride+1), + min(cols-1, cs+cstride)] + zbase = a[min(rows-1, rs+rstride), cs:min(cols, cs+cstride+1):][::-1] + zright = a[rs:min(rows-1, rs+rstride):, cs][::-1] + z = np.concatenate((ztop, zleft, zbase, zright)) + ps.append(z) + + # The construction leaves the array with duplicate points, which + # are removed here. + ps = list(zip(*ps)) + lastp = np.array([]) + ps2 = [ps[0]] + [ps[i] for i in xrange(1, len(ps)) if ps[i] != ps[i-1]] + avgzsum = sum(p[2] for p in ps2) + polys.append(ps2) + + if fcolors is not None: + colset.append(fcolors[rs][cs]) + else: + colset.append(avgzsum / len(ps2)) + + # Only need vectors to shade if no cmap + if cmap is None and shade: + i1, i2, i3 = 0, int(len(ps2)/3), int(2*len(ps2)/3) + v1[which_pt] = np.array(ps2[i1]) - np.array(ps2[i2]) + v2[which_pt] = np.array(ps2[i2]) - np.array(ps2[i3]) + which_pt += 1 + if cmap is None and shade: + normals = np.cross(v1, v2) + else : + normals = [] + + polyc = art3d.Poly3DCollection(polys, *args, **kwargs) + + if fcolors is not None: + if shade: + colset = self._shade_colors(colset, normals) + polyc.set_facecolors(colset) + polyc.set_edgecolors(colset) + elif cmap: + colset = np.array(colset) + polyc.set_array(colset) + if vmin is not None or vmax is not None: + polyc.set_clim(vmin, vmax) + if norm is not None: + polyc.set_norm(norm) + else: + if shade: + colset = self._shade_colors(color, normals) + else: + colset = color + polyc.set_facecolors(colset) + + self.add_collection(polyc) + self.auto_scale_xyz(X, Y, Z, had_data) + + return polyc + + def _generate_normals(self, polygons): + ''' + Generate normals for polygons by using the first three points. + This normal of course might not make sense for polygons with + more than three points not lying in a plane. + ''' + + normals = [] + for verts in polygons: + v1 = np.array(verts[0]) - np.array(verts[1]) + v2 = np.array(verts[2]) - np.array(verts[0]) + normals.append(np.cross(v1, v2)) + return normals + + def _shade_colors(self, color, normals): + ''' + Shade *color* using normal vectors given by *normals*. + *color* can also be an array of the same length as *normals*. + ''' + + shade = np.array([np.dot(n / proj3d.mod(n), [-1, -1, 0.5]) + if proj3d.mod(n) else np.nan + for n in normals]) + mask = ~np.isnan(shade) + + if len(shade[mask]) > 0: + norm = Normalize(min(shade[mask]), max(shade[mask])) + shade[~mask] = min(shade[mask]) + color = mcolors.to_rgba_array(color) + # shape of color should be (M, 4) (where M is number of faces) + # shape of shade should be (M,) + # colors should have final shape of (M, 4) + alpha = color[:, 3] + colors = (0.5 + norm(shade)[:, np.newaxis] * 0.5) * color + colors[:, 3] = alpha + else: + colors = np.asanyarray(color).copy() + + return colors + + def _shade_colors_lightsource(self, data, cmap, lightsource): + if lightsource is None: + lightsource = LightSource(azdeg=135, altdeg=55) + return lightsource.shade(data, cmap) + + def plot_wireframe(self, X, Y, Z, *args, **kwargs): + """ + Plot a 3D wireframe. + + .. note:: + + The *rcount* and *ccount* kwargs, which both default to 50, + determine the maximum number of samples used in each direction. If + the input data is larger, it will be downsampled (by slicing) to + these numbers of points. + + Parameters + ---------- + X, Y, Z : 2d arrays + Data values. + + rcount, ccount : int + Maximum number of samples used in each direction. If the input + data is larger, it will be downsampled (by slicing) to these + numbers of points. Setting a count to zero causes the data to be + not sampled in the corresponding direction, producing a 3D line + plot rather than a wireframe plot. Defaults to 50. + + .. versionadded:: 2.0 + + rstride, cstride : int + Downsampling stride in each direction. These arguments are + mutually exclusive with *rcount* and *ccount*. If only one of + *rstride* or *cstride* is set, the other defaults to 1. Setting a + stride to zero causes the data to be not sampled in the + corresponding direction, producing a 3D line plot rather than a + wireframe plot. + + 'classic' mode uses a default of ``rstride = cstride = 1`` instead + of the new default of ``rcount = ccount = 50``. + + **kwargs : + Other arguments are forwarded to `.Line3DCollection`. + """ + + had_data = self.has_data() + if Z.ndim != 2: + raise ValueError("Argument Z must be 2-dimensional.") + # FIXME: Support masked arrays + X, Y, Z = np.broadcast_arrays(X, Y, Z) + rows, cols = Z.shape + + has_stride = 'rstride' in kwargs or 'cstride' in kwargs + has_count = 'rcount' in kwargs or 'ccount' in kwargs + + if has_stride and has_count: + raise ValueError("Cannot specify both stride and count arguments") + + rstride = kwargs.pop('rstride', 1) + cstride = kwargs.pop('cstride', 1) + rcount = kwargs.pop('rcount', 50) + ccount = kwargs.pop('ccount', 50) + + if rcParams['_internal.classic_mode']: + # Strides have priority over counts in classic mode. + # So, only compute strides from counts + # if counts were explicitly given + if has_count: + rstride = int(max(np.ceil(rows / rcount), 1)) if rcount else 0 + cstride = int(max(np.ceil(cols / ccount), 1)) if ccount else 0 + else: + # If the strides are provided then it has priority. + # Otherwise, compute the strides from the counts. + if not has_stride: + rstride = int(max(np.ceil(rows / rcount), 1)) if rcount else 0 + cstride = int(max(np.ceil(cols / ccount), 1)) if ccount else 0 + + # We want two sets of lines, one running along the "rows" of + # Z and another set of lines running along the "columns" of Z. + # This transpose will make it easy to obtain the columns. + tX, tY, tZ = np.transpose(X), np.transpose(Y), np.transpose(Z) + + if rstride: + rii = list(xrange(0, rows, rstride)) + # Add the last index only if needed + if rows > 0 and rii[-1] != (rows - 1): + rii += [rows-1] + else: + rii = [] + if cstride: + cii = list(xrange(0, cols, cstride)) + # Add the last index only if needed + if cols > 0 and cii[-1] != (cols - 1): + cii += [cols-1] + else: + cii = [] + + if rstride == 0 and cstride == 0: + raise ValueError("Either rstride or cstride must be non zero") + + # If the inputs were empty, then just + # reset everything. + if Z.size == 0: + rii = [] + cii = [] + + xlines = [X[i] for i in rii] + ylines = [Y[i] for i in rii] + zlines = [Z[i] for i in rii] + + txlines = [tX[i] for i in cii] + tylines = [tY[i] for i in cii] + tzlines = [tZ[i] for i in cii] + + lines = ([list(zip(xl, yl, zl)) + for xl, yl, zl in zip(xlines, ylines, zlines)] + + [list(zip(xl, yl, zl)) + for xl, yl, zl in zip(txlines, tylines, tzlines)]) + + linec = art3d.Line3DCollection(lines, *args, **kwargs) + self.add_collection(linec) + self.auto_scale_xyz(X, Y, Z, had_data) + + return linec + + def plot_trisurf(self, *args, **kwargs): + """ + ============= ================================================ + Argument Description + ============= ================================================ + *X*, *Y*, *Z* Data values as 1D arrays + *color* Color of the surface patches + *cmap* A colormap for the surface patches. + *norm* An instance of Normalize to map values to colors + *vmin* Minimum value to map + *vmax* Maximum value to map + *shade* Whether to shade the facecolors + ============= ================================================ + + The (optional) triangulation can be specified in one of two ways; + either:: + + plot_trisurf(triangulation, ...) + + where triangulation is a :class:`~matplotlib.tri.Triangulation` + object, or:: + + plot_trisurf(X, Y, ...) + plot_trisurf(X, Y, triangles, ...) + plot_trisurf(X, Y, triangles=triangles, ...) + + in which case a Triangulation object will be created. See + :class:`~matplotlib.tri.Triangulation` for a explanation of + these possibilities. + + The remaining arguments are:: + + plot_trisurf(..., Z) + + where *Z* is the array of values to contour, one per point + in the triangulation. + + Other arguments are passed on to + :class:`~mpl_toolkits.mplot3d.art3d.Poly3DCollection` + + **Examples:** + + .. plot:: gallery/mplot3d/trisurf3d.py + .. plot:: gallery/mplot3d/trisurf3d_2.py + + .. versionadded:: 1.2.0 + This plotting function was added for the v1.2.0 release. + """ + + had_data = self.has_data() + + # TODO: Support custom face colours + color = kwargs.pop('color', None) + if color is None: + color = self._get_lines.get_next_color() + color = np.array(mcolors.to_rgba(color)) + + cmap = kwargs.get('cmap', None) + norm = kwargs.pop('norm', None) + vmin = kwargs.pop('vmin', None) + vmax = kwargs.pop('vmax', None) + linewidth = kwargs.get('linewidth', None) + shade = kwargs.pop('shade', cmap is None) + lightsource = kwargs.pop('lightsource', None) + + tri, args, kwargs = Triangulation.get_from_args_and_kwargs(*args, **kwargs) + if 'Z' in kwargs: + z = np.asarray(kwargs.pop('Z')) + else: + z = np.asarray(args[0]) + # We do this so Z doesn't get passed as an arg to PolyCollection + args = args[1:] + + triangles = tri.get_masked_triangles() + xt = tri.x[triangles] + yt = tri.y[triangles] + zt = z[triangles] + + # verts = np.stack((xt, yt, zt), axis=-1) + verts = np.concatenate(( + xt[..., np.newaxis], yt[..., np.newaxis], zt[..., np.newaxis] + ), axis=-1) + + polyc = art3d.Poly3DCollection(verts, *args, **kwargs) + + if cmap: + # average over the three points of each triangle + avg_z = verts[:, :, 2].mean(axis=1) + polyc.set_array(avg_z) + if vmin is not None or vmax is not None: + polyc.set_clim(vmin, vmax) + if norm is not None: + polyc.set_norm(norm) + else: + if shade: + v1 = verts[:, 0, :] - verts[:, 1, :] + v2 = verts[:, 1, :] - verts[:, 2, :] + normals = np.cross(v1, v2) + colset = self._shade_colors(color, normals) + else: + colset = color + polyc.set_facecolors(colset) + + self.add_collection(polyc) + self.auto_scale_xyz(tri.x, tri.y, z, had_data) + + return polyc + + def _3d_extend_contour(self, cset, stride=5): + ''' + Extend a contour in 3D by creating + ''' + + levels = cset.levels + colls = cset.collections + dz = (levels[1] - levels[0]) / 2 + + for z, linec in zip(levels, colls): + topverts = art3d.paths_to_3d_segments(linec.get_paths(), z - dz) + botverts = art3d.paths_to_3d_segments(linec.get_paths(), z + dz) + + color = linec.get_color()[0] + + polyverts = [] + normals = [] + nsteps = np.round(len(topverts[0]) / stride) + if nsteps <= 1: + if len(topverts[0]) > 1: + nsteps = 2 + else: + continue + + stepsize = (len(topverts[0]) - 1) / (nsteps - 1) + for i in range(int(np.round(nsteps)) - 1): + i1 = int(np.round(i * stepsize)) + i2 = int(np.round((i + 1) * stepsize)) + polyverts.append([topverts[0][i1], + topverts[0][i2], + botverts[0][i2], + botverts[0][i1]]) + + v1 = np.array(topverts[0][i1]) - np.array(topverts[0][i2]) + v2 = np.array(topverts[0][i1]) - np.array(botverts[0][i1]) + normals.append(np.cross(v1, v2)) + + colors = self._shade_colors(color, normals) + colors2 = self._shade_colors(color, normals) + polycol = art3d.Poly3DCollection(polyverts, + facecolors=colors, + edgecolors=colors2) + polycol.set_sort_zpos(z) + self.add_collection3d(polycol) + + for col in colls: + self.collections.remove(col) + + def add_contour_set(self, cset, extend3d=False, stride=5, zdir='z', offset=None): + zdir = '-' + zdir + if extend3d: + self._3d_extend_contour(cset, stride) + else: + for z, linec in zip(cset.levels, cset.collections): + if offset is not None: + z = offset + art3d.line_collection_2d_to_3d(linec, z, zdir=zdir) + + def add_contourf_set(self, cset, zdir='z', offset=None): + zdir = '-' + zdir + for z, linec in zip(cset.levels, cset.collections): + if offset is not None : + z = offset + art3d.poly_collection_2d_to_3d(linec, z, zdir=zdir) + linec.set_sort_zpos(z) + + def contour(self, X, Y, Z, *args, **kwargs): + ''' + Create a 3D contour plot. + + ========== ================================================ + Argument Description + ========== ================================================ + *X*, *Y*, Data values as numpy.arrays + *Z* + *extend3d* Whether to extend contour in 3D (default: False) + *stride* Stride (step size) for extending contour + *zdir* The direction to use: x, y or z (default) + *offset* If specified plot a projection of the contour + lines on this position in plane normal to zdir + ========== ================================================ + + The positional and other keyword arguments are passed on to + :func:`~matplotlib.axes.Axes.contour` + + Returns a :class:`~matplotlib.axes.Axes.contour` + ''' + + extend3d = kwargs.pop('extend3d', False) + stride = kwargs.pop('stride', 5) + zdir = kwargs.pop('zdir', 'z') + offset = kwargs.pop('offset', None) + + had_data = self.has_data() + + jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir) + cset = super(Axes3D, self).contour(jX, jY, jZ, *args, **kwargs) + self.add_contour_set(cset, extend3d, stride, zdir, offset) + + self.auto_scale_xyz(X, Y, Z, had_data) + return cset + + contour3D = contour + + def tricontour(self, *args, **kwargs): + """ + Create a 3D contour plot. + + ========== ================================================ + Argument Description + ========== ================================================ + *X*, *Y*, Data values as numpy.arrays + *Z* + *extend3d* Whether to extend contour in 3D (default: False) + *stride* Stride (step size) for extending contour + *zdir* The direction to use: x, y or z (default) + *offset* If specified plot a projection of the contour + lines on this position in plane normal to zdir + ========== ================================================ + + Other keyword arguments are passed on to + :func:`~matplotlib.axes.Axes.tricontour` + + Returns a :class:`~matplotlib.axes.Axes.contour` + + .. versionchanged:: 1.3.0 + Added support for custom triangulations + + EXPERIMENTAL: This method currently produces incorrect output due to a + longstanding bug in 3D PolyCollection rendering. + """ + + extend3d = kwargs.pop('extend3d', False) + stride = kwargs.pop('stride', 5) + zdir = kwargs.pop('zdir', 'z') + offset = kwargs.pop('offset', None) + + had_data = self.has_data() + + tri, args, kwargs = Triangulation.get_from_args_and_kwargs( + *args, **kwargs) + X = tri.x + Y = tri.y + if 'Z' in kwargs: + Z = kwargs.pop('Z') + else: + Z = args[0] + # We do this so Z doesn't get passed as an arg to Axes.tricontour + args = args[1:] + + jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir) + tri = Triangulation(jX, jY, tri.triangles, tri.mask) + + cset = super(Axes3D, self).tricontour(tri, jZ, *args, **kwargs) + self.add_contour_set(cset, extend3d, stride, zdir, offset) + + self.auto_scale_xyz(X, Y, Z, had_data) + return cset + + def contourf(self, X, Y, Z, *args, **kwargs): + ''' + Create a 3D contourf plot. + + ========== ================================================ + Argument Description + ========== ================================================ + *X*, *Y*, Data values as numpy.arrays + *Z* + *zdir* The direction to use: x, y or z (default) + *offset* If specified plot a projection of the filled contour + on this position in plane normal to zdir + ========== ================================================ + + The positional and keyword arguments are passed on to + :func:`~matplotlib.axes.Axes.contourf` + + Returns a :class:`~matplotlib.axes.Axes.contourf` + + .. versionchanged :: 1.1.0 + The *zdir* and *offset* kwargs were added. + ''' + + zdir = kwargs.pop('zdir', 'z') + offset = kwargs.pop('offset', None) + + had_data = self.has_data() + + jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir) + cset = super(Axes3D, self).contourf(jX, jY, jZ, *args, **kwargs) + self.add_contourf_set(cset, zdir, offset) + + self.auto_scale_xyz(X, Y, Z, had_data) + return cset + + contourf3D = contourf + + def tricontourf(self, *args, **kwargs): + """ + Create a 3D contourf plot. + + ========== ================================================ + Argument Description + ========== ================================================ + *X*, *Y*, Data values as numpy.arrays + *Z* + *zdir* The direction to use: x, y or z (default) + *offset* If specified plot a projection of the contour + lines on this position in plane normal to zdir + ========== ================================================ + + Other keyword arguments are passed on to + :func:`~matplotlib.axes.Axes.tricontour` + + Returns a :class:`~matplotlib.axes.Axes.contour` + + .. versionchanged :: 1.3.0 + Added support for custom triangulations + + EXPERIMENTAL: This method currently produces incorrect output due to a + longstanding bug in 3D PolyCollection rendering. + """ + zdir = kwargs.pop('zdir', 'z') + offset = kwargs.pop('offset', None) + + had_data = self.has_data() + + tri, args, kwargs = Triangulation.get_from_args_and_kwargs( + *args, **kwargs) + X = tri.x + Y = tri.y + if 'Z' in kwargs: + Z = kwargs.pop('Z') + else: + Z = args[0] + # We do this so Z doesn't get passed as an arg to Axes.tricontourf + args = args[1:] + + jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir) + tri = Triangulation(jX, jY, tri.triangles, tri.mask) + + cset = super(Axes3D, self).tricontourf(tri, jZ, *args, **kwargs) + self.add_contourf_set(cset, zdir, offset) + + self.auto_scale_xyz(X, Y, Z, had_data) + return cset + + def add_collection3d(self, col, zs=0, zdir='z'): + ''' + Add a 3D collection object to the plot. + + 2D collection types are converted to a 3D version by + modifying the object and adding z coordinate information. + + Supported are: + - PolyCollection + - LineCollection + - PatchCollection + ''' + zvals = np.atleast_1d(zs) + if len(zvals) > 0 : + zsortval = min(zvals) + else : + zsortval = 0 # FIXME: Fairly arbitrary. Is there a better value? + + # FIXME: use issubclass() (although, then a 3D collection + # object would also pass.) Maybe have a collection3d + # abstract class to test for and exclude? + if type(col) is mcoll.PolyCollection: + art3d.poly_collection_2d_to_3d(col, zs=zs, zdir=zdir) + col.set_sort_zpos(zsortval) + elif type(col) is mcoll.LineCollection: + art3d.line_collection_2d_to_3d(col, zs=zs, zdir=zdir) + col.set_sort_zpos(zsortval) + elif type(col) is mcoll.PatchCollection: + art3d.patch_collection_2d_to_3d(col, zs=zs, zdir=zdir) + col.set_sort_zpos(zsortval) + + super(Axes3D, self).add_collection(col) + + def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True, + *args, **kwargs): + ''' + Create a scatter plot. + + ============ ======================================================== + Argument Description + ============ ======================================================== + *xs*, *ys* Positions of data points. + *zs* Either an array of the same length as *xs* and + *ys* or a single value to place all points in + the same plane. Default is 0. + *zdir* Which direction to use as z ('x', 'y' or 'z') + when plotting a 2D set. + *s* Size in points^2. It is a scalar or an array of the + same length as *x* and *y*. + + *c* A color. *c* can be a single color format string, or a + sequence of color specifications of length *N*, or a + sequence of *N* numbers to be mapped to colors using the + *cmap* and *norm* specified via kwargs (see below). Note + that *c* should not be a single numeric RGB or RGBA + sequence because that is indistinguishable from an array + of values to be colormapped. *c* can be a 2-D array in + which the rows are RGB or RGBA, however, including the + case of a single row to specify the same color for + all points. + + *depthshade* + Whether or not to shade the scatter markers to give + the appearance of depth. Default is *True*. + ============ ======================================================== + + Keyword arguments are passed on to + :func:`~matplotlib.axes.Axes.scatter`. + + Returns a :class:`~mpl_toolkits.mplot3d.art3d.Patch3DCollection` + ''' + + had_data = self.has_data() + + xs, ys, zs = np.broadcast_arrays( + *[np.ravel(np.ma.filled(t, np.nan)) for t in [xs, ys, zs]]) + s = np.ma.ravel(s) # This doesn't have to match x, y in size. + + xs, ys, zs, s, c = cbook.delete_masked_points(xs, ys, zs, s, c) + + patches = super(Axes3D, self).scatter( + xs, ys, s=s, c=c, *args, **kwargs) + is_2d = not cbook.iterable(zs) + zs = _backports.broadcast_to(zs, len(xs)) + art3d.patch_collection_2d_to_3d(patches, zs=zs, zdir=zdir, + depthshade=depthshade) + + if self._zmargin < 0.05 and xs.size > 0: + self.set_zmargin(0.05) + + #FIXME: why is this necessary? + if not is_2d: + self.auto_scale_xyz(xs, ys, zs, had_data) + + return patches + + scatter3D = scatter + + def bar(self, left, height, zs=0, zdir='z', *args, **kwargs): + ''' + Add 2D bar(s). + + ========== ================================================ + Argument Description + ========== ================================================ + *left* The x coordinates of the left sides of the bars. + *height* The height of the bars. + *zs* Z coordinate of bars, if one value is specified + they will all be placed at the same z. + *zdir* Which direction to use as z ('x', 'y' or 'z') + when plotting a 2D set. + ========== ================================================ + + Keyword arguments are passed onto :func:`~matplotlib.axes.Axes.bar`. + + Returns a :class:`~mpl_toolkits.mplot3d.art3d.Patch3DCollection` + ''' + + had_data = self.has_data() + + patches = super(Axes3D, self).bar(left, height, *args, **kwargs) + + zs = _backports.broadcast_to(zs, len(left)) + + verts = [] + verts_zs = [] + for p, z in zip(patches, zs): + vs = art3d.get_patch_verts(p) + verts += vs.tolist() + verts_zs += [z] * len(vs) + art3d.patch_2d_to_3d(p, z, zdir) + if 'alpha' in kwargs: + p.set_alpha(kwargs['alpha']) + + if len(verts) > 0 : + # the following has to be skipped if verts is empty + # NOTE: Bugs could still occur if len(verts) > 0, + # but the "2nd dimension" is empty. + xs, ys = list(zip(*verts)) + else : + xs, ys = [], [] + + xs, ys, verts_zs = art3d.juggle_axes(xs, ys, verts_zs, zdir) + self.auto_scale_xyz(xs, ys, verts_zs, had_data) + + return patches + + def bar3d(self, x, y, z, dx, dy, dz, color=None, + zsort='average', shade=True, *args, **kwargs): + """Generate a 3D barplot. + + This method creates three dimensional barplot where the width, + depth, height, and color of the bars can all be uniquely set. + + Parameters + ---------- + x, y, z : array-like + The coordinates of the anchor point of the bars. + + dx, dy, dz : scalar or array-like + The width, depth, and height of the bars, respectively. + + color : sequence of valid color specifications, optional + The color of the bars can be specified globally or + individually. This parameter can be: + + - A single color value, to color all bars the same color. + - An array of colors of length N bars, to color each bar + independently. + - An array of colors of length 6, to color the faces of the + bars similarly. + - An array of colors of length 6 * N bars, to color each face + independently. + + When coloring the faces of the boxes specifically, this is + the order of the coloring: + + 1. -Z (bottom of box) + 2. +Z (top of box) + 3. -Y + 4. +Y + 5. -X + 6. +X + + zsort : str, optional + The z-axis sorting scheme passed onto + :func:`~mpl_toolkits.mplot3d.art3d.Poly3DCollection` + + shade : bool, optional (default = True) + When true, this shades the dark sides of the bars (relative + to the plot's source of light). + + Any additional keyword arguments are passed onto + :func:`~mpl_toolkits.mplot3d.art3d.Poly3DCollection` + + Returns + ------- + collection : Poly3DCollection + A collection of three dimensional polygons representing + the bars. + """ + + had_data = self.has_data() + + x, y, z, dx, dy, dz = np.broadcast_arrays( + np.atleast_1d(x), y, z, dx, dy, dz) + minx = np.min(x) + maxx = np.max(x + dx) + miny = np.min(y) + maxy = np.max(y + dy) + minz = np.min(z) + maxz = np.max(z + dz) + + polys = [] + for xi, yi, zi, dxi, dyi, dzi in zip(x, y, z, dx, dy, dz): + polys.extend([ + ((xi, yi, zi), (xi + dxi, yi, zi), + (xi + dxi, yi + dyi, zi), (xi, yi + dyi, zi)), + ((xi, yi, zi + dzi), (xi + dxi, yi, zi + dzi), + (xi + dxi, yi + dyi, zi + dzi), (xi, yi + dyi, zi + dzi)), + + ((xi, yi, zi), (xi + dxi, yi, zi), + (xi + dxi, yi, zi + dzi), (xi, yi, zi + dzi)), + ((xi, yi + dyi, zi), (xi + dxi, yi + dyi, zi), + (xi + dxi, yi + dyi, zi + dzi), (xi, yi + dyi, zi + dzi)), + + ((xi, yi, zi), (xi, yi + dyi, zi), + (xi, yi + dyi, zi + dzi), (xi, yi, zi + dzi)), + ((xi + dxi, yi, zi), (xi + dxi, yi + dyi, zi), + (xi + dxi, yi + dyi, zi + dzi), (xi + dxi, yi, zi + dzi)), + ]) + + facecolors = [] + if color is None: + color = [self._get_patches_for_fill.get_next_color()] + + if len(color) == len(x): + # bar colors specified, need to expand to number of faces + for c in color: + facecolors.extend([c] * 6) + else: + # a single color specified, or face colors specified explicitly + facecolors = list(mcolors.to_rgba_array(color)) + if len(facecolors) < len(x): + facecolors *= (6 * len(x)) + + if shade: + normals = self._generate_normals(polys) + sfacecolors = self._shade_colors(facecolors, normals) + else: + sfacecolors = facecolors + + col = art3d.Poly3DCollection(polys, + zsort=zsort, + facecolor=sfacecolors, + *args, **kwargs) + self.add_collection(col) + + self.auto_scale_xyz((minx, maxx), (miny, maxy), (minz, maxz), had_data) + + return col + + def set_title(self, label, fontdict=None, loc='center', **kwargs): + ret = super(Axes3D, self).set_title(label, fontdict=fontdict, loc=loc, + **kwargs) + (x, y) = self.title.get_position() + self.title.set_y(0.92 * y) + return ret + set_title.__doc__ = maxes.Axes.set_title.__doc__ + + def quiver(self, *args, **kwargs): + """ + Plot a 3D field of arrows. + + call signatures:: + + quiver(X, Y, Z, U, V, W, **kwargs) + + Arguments: + + *X*, *Y*, *Z*: + The x, y and z coordinates of the arrow locations (default is + tail of arrow; see *pivot* kwarg) + + *U*, *V*, *W*: + The x, y and z components of the arrow vectors + + The arguments could be array-like or scalars, so long as they + they can be broadcast together. The arguments can also be + masked arrays. If an element in any of argument is masked, then + that corresponding quiver element will not be plotted. + + Keyword arguments: + + *length*: [1.0 | float] + The length of each quiver, default to 1.0, the unit is + the same with the axes + + *arrow_length_ratio*: [0.3 | float] + The ratio of the arrow head with respect to the quiver, + default to 0.3 + + *pivot*: [ 'tail' | 'middle' | 'tip' ] + The part of the arrow that is at the grid point; the arrow + rotates about this point, hence the name *pivot*. + Default is 'tail' + + *normalize*: bool + When True, all of the arrows will be the same length. This + defaults to False, where the arrows will be different lengths + depending on the values of u,v,w. + + Any additional keyword arguments are delegated to + :class:`~matplotlib.collections.LineCollection` + + """ + def calc_arrow(uvw, angle=15): + """ + To calculate the arrow head. uvw should be a unit vector. + We normalize it here: + """ + # get unit direction vector perpendicular to (u,v,w) + norm = np.linalg.norm(uvw[:2]) + if norm > 0: + x = uvw[1] / norm + y = -uvw[0] / norm + else: + x, y = 0, 1 + + # compute the two arrowhead direction unit vectors + ra = math.radians(angle) + c = math.cos(ra) + s = math.sin(ra) + + # construct the rotation matrices + Rpos = np.array([[c+(x**2)*(1-c), x*y*(1-c), y*s], + [y*x*(1-c), c+(y**2)*(1-c), -x*s], + [-y*s, x*s, c]]) + # opposite rotation negates all the sin terms + Rneg = Rpos.copy() + Rneg[[0,1,2,2],[2,2,0,1]] = -Rneg[[0,1,2,2],[2,2,0,1]] + + # multiply them to get the rotated vector + return Rpos.dot(uvw), Rneg.dot(uvw) + + had_data = self.has_data() + + # handle kwargs + # shaft length + length = kwargs.pop('length', 1) + # arrow length ratio to the shaft length + arrow_length_ratio = kwargs.pop('arrow_length_ratio', 0.3) + # pivot point + pivot = kwargs.pop('pivot', 'tail') + # normalize + normalize = kwargs.pop('normalize', False) + + # handle args + argi = 6 + if len(args) < argi: + raise ValueError('Wrong number of arguments. Expected %d got %d' % + (argi, len(args))) + + # first 6 arguments are X, Y, Z, U, V, W + input_args = args[:argi] + # if any of the args are scalar, convert into list + input_args = [[k] if isinstance(k, (int, float)) else k + for k in input_args] + + # extract the masks, if any + masks = [k.mask for k in input_args if isinstance(k, np.ma.MaskedArray)] + # broadcast to match the shape + bcast = np.broadcast_arrays(*(input_args + masks)) + input_args = bcast[:argi] + masks = bcast[argi:] + if masks: + # combine the masks into one + mask = reduce(np.logical_or, masks) + # put mask on and compress + input_args = [np.ma.array(k, mask=mask).compressed() + for k in input_args] + else: + input_args = [k.flatten() for k in input_args] + + if any(len(v) == 0 for v in input_args): + # No quivers, so just make an empty collection and return early + linec = art3d.Line3DCollection([], *args[argi:], **kwargs) + self.add_collection(linec) + return linec + + # Following assertions must be true before proceeding + # must all be ndarray + assert all(isinstance(k, np.ndarray) for k in input_args) + # must all in same shape + assert len({k.shape for k in input_args}) == 1 + + shaft_dt = np.linspace(0, length, num=2) + arrow_dt = shaft_dt * arrow_length_ratio + + if pivot == 'tail': + shaft_dt -= length + elif pivot == 'middle': + shaft_dt -= length/2. + elif pivot != 'tip': + raise ValueError('Invalid pivot argument: ' + str(pivot)) + + XYZ = np.column_stack(input_args[:3]) + UVW = np.column_stack(input_args[3:argi]).astype(float) + + # Normalize rows of UVW + # Note: with numpy 1.9+, could use np.linalg.norm(UVW, axis=1) + norm = np.sqrt(np.sum(UVW**2, axis=1)) + + # If any row of UVW is all zeros, don't make a quiver for it + mask = norm > 0 + XYZ = XYZ[mask] + if normalize: + UVW = UVW[mask] / norm[mask].reshape((-1, 1)) + else: + UVW = UVW[mask] + + if len(XYZ) > 0: + # compute the shaft lines all at once with an outer product + shafts = (XYZ - np.multiply.outer(shaft_dt, UVW)).swapaxes(0, 1) + # compute head direction vectors, n heads by 2 sides by 3 dimensions + head_dirs = np.array([calc_arrow(d) for d in UVW]) + # compute all head lines at once, starting from where the shaft ends + heads = shafts[:, :1] - np.multiply.outer(arrow_dt, head_dirs) + # stack left and right head lines together + heads.shape = (len(arrow_dt), -1, 3) + # transpose to get a list of lines + heads = heads.swapaxes(0, 1) + + lines = list(shafts) + list(heads) + else: + lines = [] + + linec = art3d.Line3DCollection(lines, *args[argi:], **kwargs) + self.add_collection(linec) + + self.auto_scale_xyz(XYZ[:, 0], XYZ[:, 1], XYZ[:, 2], had_data) + + return linec + + quiver3D = quiver + + def voxels(self, *args, **kwargs): + """ + ax.voxels([x, y, z,] /, filled, **kwargs) + + Plot a set of filled voxels + + All voxels are plotted as 1x1x1 cubes on the axis, with filled[0,0,0] + placed with its lower corner at the origin. Occluded faces are not + plotted. + + Call signatures:: + + voxels(filled, facecolors=fc, edgecolors=ec, **kwargs) + voxels(x, y, z, filled, facecolors=fc, edgecolors=ec, **kwargs) + + .. versionadded:: 2.1 + + Parameters + ---------- + filled : 3D np.array of bool + A 3d array of values, with truthy values indicating which voxels + to fill + + x, y, z : 3D np.array, optional + The coordinates of the corners of the voxels. This should broadcast + to a shape one larger in every dimension than the shape of `filled`. + These can be used to plot non-cubic voxels. + + If not specified, defaults to increasing integers along each axis, + like those returned by :func:`~numpy.indices`. + As indicated by the ``/`` in the function signature, these arguments + can only be passed positionally. + + facecolors, edgecolors : array_like, optional + The color to draw the faces and edges of the voxels. Can only be + passed as keyword arguments. + This parameter can be: + + - A single color value, to color all voxels the same color. This + can be either a string, or a 1D rgb/rgba array + - ``None``, the default, to use a single color for the faces, and + the style default for the edges. + - A 3D ndarray of color names, with each item the color for the + corresponding voxel. The size must match the voxels. + - A 4D ndarray of rgb/rgba data, with the components along the + last axis. + + **kwargs + Additional keyword arguments to pass onto + :func:`~mpl_toolkits.mplot3d.art3d.Poly3DCollection` + + Returns + ------- + faces : dict + A dictionary indexed by coordinate, where ``faces[i,j,k]`` is a + `Poly3DCollection` of the faces drawn for the voxel + ``filled[i,j,k]``. If no faces were drawn for a given voxel, either + because it was not asked to be drawn, or it is fully occluded, then + ``(i,j,k) not in faces``. + + Examples + -------- + .. plot:: gallery/mplot3d/voxels.py + .. plot:: gallery/mplot3d/voxels_rgb.py + .. plot:: gallery/mplot3d/voxels_torus.py + .. plot:: gallery/mplot3d/voxels_numpy_logo.py + """ + + # work out which signature we should be using, and use it to parse + # the arguments. Name must be voxels for the correct error message + if len(args) >= 3: + # underscores indicate position only + def voxels(__x, __y, __z, filled, **kwargs): + return (__x, __y, __z), filled, kwargs + else: + def voxels(filled, **kwargs): + return None, filled, kwargs + + xyz, filled, kwargs = voxels(*args, **kwargs) + + # check dimensions + if filled.ndim != 3: + raise ValueError("Argument filled must be 3-dimensional") + size = np.array(filled.shape, dtype=np.intp) + + # check xyz coordinates, which are one larger than the filled shape + coord_shape = tuple(size + 1) + if xyz is None: + x, y, z = np.indices(coord_shape) + else: + x, y, z = (_backports.broadcast_to(c, coord_shape) for c in xyz) + + def _broadcast_color_arg(color, name): + if np.ndim(color) in (0, 1): + # single color, like "red" or [1, 0, 0] + return _backports.broadcast_to( + color, filled.shape + np.shape(color)) + elif np.ndim(color) in (3, 4): + # 3D array of strings, or 4D array with last axis rgb + if np.shape(color)[:3] != filled.shape: + raise ValueError( + "When multidimensional, {} must match the shape of " + "filled".format(name)) + return color + else: + raise ValueError("Invalid {} argument".format(name)) + + # intercept the facecolors, handling defaults and broacasting + facecolors = kwargs.pop('facecolors', None) + if facecolors is None: + facecolors = self._get_patches_for_fill.get_next_color() + facecolors = _broadcast_color_arg(facecolors, 'facecolors') + + # broadcast but no default on edgecolors + edgecolors = kwargs.pop('edgecolors', None) + edgecolors = _broadcast_color_arg(edgecolors, 'edgecolors') + + # always scale to the full array, even if the data is only in the center + self.auto_scale_xyz(x, y, z) + + # points lying on corners of a square + square = np.array([ + [0, 0, 0], + [0, 1, 0], + [1, 1, 0], + [1, 0, 0] + ], dtype=np.intp) + + voxel_faces = defaultdict(list) + + def permutation_matrices(n): + """ Generator of cyclic permutation matices """ + mat = np.eye(n, dtype=np.intp) + for i in range(n): + yield mat + mat = np.roll(mat, 1, axis=0) + + # iterate over each of the YZ, ZX, and XY orientations, finding faces to + # render + for permute in permutation_matrices(3): + # find the set of ranges to iterate over + pc, qc, rc = permute.T.dot(size) + pinds = np.arange(pc) + qinds = np.arange(qc) + rinds = np.arange(rc) + + square_rot = square.dot(permute.T) + + # iterate within the current plane + for p in pinds: + for q in qinds: + # iterate perpendicularly to the current plane, handling + # boundaries. We only draw faces between a voxel and an + # empty space, to avoid drawing internal faces. + + # draw lower faces + p0 = permute.dot([p, q, 0]) + i0 = tuple(p0) + if filled[i0]: + voxel_faces[i0].append(p0 + square_rot) + + # draw middle faces + for r1, r2 in zip(rinds[:-1], rinds[1:]): + p1 = permute.dot([p, q, r1]) + p2 = permute.dot([p, q, r2]) + + i1 = tuple(p1) + i2 = tuple(p2) + + if filled[i1] and not filled[i2]: + voxel_faces[i1].append(p2 + square_rot) + elif not filled[i1] and filled[i2]: + voxel_faces[i2].append(p2 + square_rot) + + # draw upper faces + pk = permute.dot([p, q, rc-1]) + pk2 = permute.dot([p, q, rc]) + ik = tuple(pk) + if filled[ik]: + voxel_faces[ik].append(pk2 + square_rot) + + # iterate over the faces, and generate a Poly3DCollection for each voxel + polygons = {} + for coord, faces_inds in voxel_faces.items(): + # convert indices into 3D positions + if xyz is None: + faces = faces_inds + else: + faces = [] + for face_inds in faces_inds: + ind = face_inds[:, 0], face_inds[:, 1], face_inds[:, 2] + face = np.empty(face_inds.shape) + face[:, 0] = x[ind] + face[:, 1] = y[ind] + face[:, 2] = z[ind] + faces.append(face) + + poly = art3d.Poly3DCollection(faces, + facecolors=facecolors[coord], + edgecolors=edgecolors[coord], + **kwargs + ) + self.add_collection3d(poly) + polygons[coord] = poly + + return polygons + + +def get_test_data(delta=0.05): + ''' + Return a tuple X, Y, Z with a test data set. + ''' + x = y = np.arange(-3.0, 3.0, delta) + X, Y = np.meshgrid(x, y) + + Z1 = np.exp(-(X**2 + Y**2) / 2) / (2 * np.pi) + Z2 = (np.exp(-(((X - 1) / 1.5)**2 + ((Y - 1) / 0.5)**2) / 2) / + (2 * np.pi * 0.5 * 1.5)) + Z = Z2 - Z1 + + X = X * 10 + Y = Y * 10 + Z = Z * 500 + return X, Y, Z + + +######################################################## +# Register Axes3D as a 'projection' object available +# for use just like any other axes +######################################################## +import matplotlib.projections as proj +proj.projection_registry.register(Axes3D) diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/axis3d.py b/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/axis3d.py new file mode 100644 index 0000000000..50b81df912 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/axis3d.py @@ -0,0 +1,484 @@ +# axis3d.py, original mplot3d version by John Porter +# Created: 23 Sep 2005 +# Parts rewritten by Reinier Heeres <reinier@heeres.eu> + +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six + +import math +import copy + +from matplotlib import lines as mlines, axis as maxis, patches as mpatches +from matplotlib import rcParams +from . import art3d +from . import proj3d + +import numpy as np + +def get_flip_min_max(coord, index, mins, maxs): + if coord[index] == mins[index]: + return maxs[index] + else: + return mins[index] + +def move_from_center(coord, centers, deltas, axmask=(True, True, True)): + '''Return a coordinate that is moved by "deltas" away from the center.''' + coord = copy.copy(coord) + for i in range(3): + if not axmask[i]: + continue + if coord[i] < centers[i]: + coord[i] -= deltas[i] + else: + coord[i] += deltas[i] + return coord + +def tick_update_position(tick, tickxs, tickys, labelpos): + '''Update tick line and label position and style.''' + + for (label, on) in [(tick.label1, tick.label1On), + (tick.label2, tick.label2On)]: + if on: + label.set_position(labelpos) + + tick.tick1On, tick.tick2On = True, False + tick.tick1line.set_linestyle('-') + tick.tick1line.set_marker('') + tick.tick1line.set_data(tickxs, tickys) + tick.gridline.set_data(0, 0) + +class Axis(maxis.XAxis): + + # These points from the unit cube make up the x, y and z-planes + _PLANES = ( + (0, 3, 7, 4), (1, 2, 6, 5), # yz planes + (0, 1, 5, 4), (3, 2, 6, 7), # xz planes + (0, 1, 2, 3), (4, 5, 6, 7), # xy planes + ) + + # Some properties for the axes + _AXINFO = { + 'x': {'i': 0, 'tickdir': 1, 'juggled': (1, 0, 2), + 'color': (0.95, 0.95, 0.95, 0.5)}, + 'y': {'i': 1, 'tickdir': 0, 'juggled': (0, 1, 2), + 'color': (0.90, 0.90, 0.90, 0.5)}, + 'z': {'i': 2, 'tickdir': 0, 'juggled': (0, 2, 1), + 'color': (0.925, 0.925, 0.925, 0.5)}, + } + + def __init__(self, adir, v_intervalx, d_intervalx, axes, *args, **kwargs): + # adir identifies which axes this is + self.adir = adir + # data and viewing intervals for this direction + self.d_interval = d_intervalx + self.v_interval = v_intervalx + + # This is a temporary member variable. + # Do not depend on this existing in future releases! + self._axinfo = self._AXINFO[adir].copy() + if rcParams['_internal.classic_mode']: + self._axinfo.update( + {'label': {'va': 'center', + 'ha': 'center'}, + 'tick': {'inward_factor': 0.2, + 'outward_factor': 0.1, + 'linewidth': rcParams['lines.linewidth'], + 'color': 'k'}, + 'axisline': {'linewidth': 0.75, + 'color': (0, 0, 0, 1)}, + 'grid': {'color': (0.9, 0.9, 0.9, 1), + 'linewidth': 1.0, + 'linestyle': '-'}, + }) + else: + self._axinfo.update( + {'label': {'va': 'center', + 'ha': 'center'}, + 'tick': {'inward_factor': 0.2, + 'outward_factor': 0.1, + 'linewidth': rcParams.get( + adir + 'tick.major.width', + rcParams['xtick.major.width']), + 'color': rcParams.get( + adir + 'tick.color', + rcParams['xtick.color'])}, + 'axisline': {'linewidth': rcParams['axes.linewidth'], + 'color': rcParams['axes.edgecolor']}, + 'grid': {'color': rcParams['grid.color'], + 'linewidth': rcParams['grid.linewidth'], + 'linestyle': rcParams['grid.linestyle']}, + }) + + maxis.XAxis.__init__(self, axes, *args, **kwargs) + self.set_rotate_label(kwargs.get('rotate_label', None)) + + def init3d(self): + self.line = mlines.Line2D( + xdata=(0, 0), ydata=(0, 0), + linewidth=self._axinfo['axisline']['linewidth'], + color=self._axinfo['axisline']['color'], + antialiased=True) + + # Store dummy data in Polygon object + self.pane = mpatches.Polygon( + np.array([[0, 0], [0, 1], [1, 0], [0, 0]]), + closed=False, alpha=0.8, facecolor='k', edgecolor='k') + self.set_pane_color(self._axinfo['color']) + + self.axes._set_artist_props(self.line) + self.axes._set_artist_props(self.pane) + self.gridlines = art3d.Line3DCollection([]) + self.axes._set_artist_props(self.gridlines) + self.axes._set_artist_props(self.label) + self.axes._set_artist_props(self.offsetText) + # Need to be able to place the label at the correct location + self.label._transform = self.axes.transData + self.offsetText._transform = self.axes.transData + + def get_tick_positions(self): + majorLocs = self.major.locator() + self.major.formatter.set_locs(majorLocs) + majorLabels = [self.major.formatter(val, i) + for i, val in enumerate(majorLocs)] + return majorLabels, majorLocs + + def get_major_ticks(self, numticks=None): + ticks = maxis.XAxis.get_major_ticks(self, numticks) + for t in ticks: + t.tick1line.set_transform(self.axes.transData) + t.tick2line.set_transform(self.axes.transData) + t.gridline.set_transform(self.axes.transData) + t.label1.set_transform(self.axes.transData) + t.label2.set_transform(self.axes.transData) + return ticks + + def set_pane_pos(self, xys): + xys = np.asarray(xys) + xys = xys[:,:2] + self.pane.xy = xys + self.stale = True + + def set_pane_color(self, color): + '''Set pane color to a RGBA tuple.''' + self._axinfo['color'] = color + self.pane.set_edgecolor(color) + self.pane.set_facecolor(color) + self.pane.set_alpha(color[-1]) + self.stale = True + + def set_rotate_label(self, val): + ''' + Whether to rotate the axis label: True, False or None. + If set to None the label will be rotated if longer than 4 chars. + ''' + self._rotate_label = val + self.stale = True + + def get_rotate_label(self, text): + if self._rotate_label is not None: + return self._rotate_label + else: + return len(text) > 4 + + def _get_coord_info(self, renderer): + minx, maxx, miny, maxy, minz, maxz = self.axes.get_w_lims() + if minx > maxx: + minx, maxx = maxx, minx + if miny > maxy: + miny, maxy = maxy, miny + if minz > maxz: + minz, maxz = maxz, minz + mins = np.array((minx, miny, minz)) + maxs = np.array((maxx, maxy, maxz)) + centers = (maxs + mins) / 2. + deltas = (maxs - mins) / 12. + mins = mins - deltas / 4. + maxs = maxs + deltas / 4. + + vals = mins[0], maxs[0], mins[1], maxs[1], mins[2], maxs[2] + tc = self.axes.tunit_cube(vals, renderer.M) + avgz = [tc[p1][2] + tc[p2][2] + tc[p3][2] + tc[p4][2] + for p1, p2, p3, p4 in self._PLANES] + highs = np.array([avgz[2*i] < avgz[2*i+1] for i in range(3)]) + + return mins, maxs, centers, deltas, tc, highs + + def draw_pane(self, renderer): + renderer.open_group('pane3d') + + mins, maxs, centers, deltas, tc, highs = self._get_coord_info(renderer) + + info = self._axinfo + index = info['i'] + if not highs[index]: + plane = self._PLANES[2 * index] + else: + plane = self._PLANES[2 * index + 1] + xys = [tc[p] for p in plane] + self.set_pane_pos(xys) + self.pane.draw(renderer) + + renderer.close_group('pane3d') + + def draw(self, renderer): + self.label._transform = self.axes.transData + renderer.open_group('axis3d') + + # code from XAxis + majorTicks = self.get_major_ticks() + majorLocs = self.major.locator() + + info = self._axinfo + index = info['i'] + + # filter locations here so that no extra grid lines are drawn + locmin, locmax = self.get_view_interval() + if locmin > locmax: + locmin, locmax = locmax, locmin + + # Rudimentary clipping + majorLocs = [loc for loc in majorLocs if + locmin <= loc <= locmax] + self.major.formatter.set_locs(majorLocs) + majorLabels = [self.major.formatter(val, i) + for i, val in enumerate(majorLocs)] + + mins, maxs, centers, deltas, tc, highs = self._get_coord_info(renderer) + + # Determine grid lines + minmax = np.where(highs, maxs, mins) + + # Draw main axis line + juggled = info['juggled'] + edgep1 = minmax.copy() + edgep1[juggled[0]] = get_flip_min_max(edgep1, juggled[0], mins, maxs) + + edgep2 = edgep1.copy() + edgep2[juggled[1]] = get_flip_min_max(edgep2, juggled[1], mins, maxs) + pep = proj3d.proj_trans_points([edgep1, edgep2], renderer.M) + centpt = proj3d.proj_transform( + centers[0], centers[1], centers[2], renderer.M) + self.line.set_data((pep[0][0], pep[0][1]), (pep[1][0], pep[1][1])) + self.line.draw(renderer) + + # Grid points where the planes meet + xyz0 = [] + for val in majorLocs: + coord = minmax.copy() + coord[index] = val + xyz0.append(coord) + + # Draw labels + peparray = np.asanyarray(pep) + # The transAxes transform is used because the Text object + # rotates the text relative to the display coordinate system. + # Therefore, if we want the labels to remain parallel to the + # axis regardless of the aspect ratio, we need to convert the + # edge points of the plane to display coordinates and calculate + # an angle from that. + # TODO: Maybe Text objects should handle this themselves? + dx, dy = (self.axes.transAxes.transform([peparray[0:2, 1]]) - + self.axes.transAxes.transform([peparray[0:2, 0]]))[0] + + lxyz = 0.5*(edgep1 + edgep2) + + # A rough estimate; points are ambiguous since 3D plots rotate + ax_scale = self.axes.bbox.size / self.figure.bbox.size + ax_inches = np.multiply(ax_scale, self.figure.get_size_inches()) + ax_points_estimate = sum(72. * ax_inches) + deltas_per_point = 48. / ax_points_estimate + default_offset = 21. + labeldeltas = ( + (self.labelpad + default_offset) * deltas_per_point * deltas) + axmask = [True, True, True] + axmask[index] = False + lxyz = move_from_center(lxyz, centers, labeldeltas, axmask) + tlx, tly, tlz = proj3d.proj_transform(lxyz[0], lxyz[1], lxyz[2], + renderer.M) + self.label.set_position((tlx, tly)) + if self.get_rotate_label(self.label.get_text()): + angle = art3d.norm_text_angle(math.degrees(math.atan2(dy, dx))) + self.label.set_rotation(angle) + self.label.set_va(info['label']['va']) + self.label.set_ha(info['label']['ha']) + self.label.draw(renderer) + + + # Draw Offset text + + # Which of the two edge points do we want to + # use for locating the offset text? + if juggled[2] == 2 : + outeredgep = edgep1 + outerindex = 0 + else : + outeredgep = edgep2 + outerindex = 1 + + pos = copy.copy(outeredgep) + pos = move_from_center(pos, centers, labeldeltas, axmask) + olx, oly, olz = proj3d.proj_transform( + pos[0], pos[1], pos[2], renderer.M) + self.offsetText.set_text( self.major.formatter.get_offset() ) + self.offsetText.set_position( (olx, oly) ) + angle = art3d.norm_text_angle(math.degrees(math.atan2(dy, dx))) + self.offsetText.set_rotation(angle) + # Must set rotation mode to "anchor" so that + # the alignment point is used as the "fulcrum" for rotation. + self.offsetText.set_rotation_mode('anchor') + + #---------------------------------------------------------------------- + # Note: the following statement for determining the proper alignment of + # the offset text. This was determined entirely by trial-and-error + # and should not be in any way considered as "the way". There are + # still some edge cases where alignment is not quite right, but this + # seems to be more of a geometry issue (in other words, I might be + # using the wrong reference points). + # + # (TT, FF, TF, FT) are the shorthand for the tuple of + # (centpt[info['tickdir']] <= peparray[info['tickdir'], outerindex], + # centpt[index] <= peparray[index, outerindex]) + # + # Three-letters (e.g., TFT, FTT) are short-hand for the array of bools + # from the variable 'highs'. + # --------------------------------------------------------------------- + if centpt[info['tickdir']] > peparray[info['tickdir'], outerindex] : + # if FT and if highs has an even number of Trues + if (centpt[index] <= peparray[index, outerindex] + and ((len(highs.nonzero()[0]) % 2) == 0)) : + # Usually, this means align right, except for the FTT case, + # in which offset for axis 1 and 2 are aligned left. + if highs.tolist() == [False, True, True] and index in (1, 2) : + align = 'left' + else : + align = 'right' + else : + # The FF case + align = 'left' + else : + # if TF and if highs has an even number of Trues + if (centpt[index] > peparray[index, outerindex] + and ((len(highs.nonzero()[0]) % 2) == 0)) : + # Usually mean align left, except if it is axis 2 + if index == 2 : + align = 'right' + else : + align = 'left' + else : + # The TT case + align = 'right' + + self.offsetText.set_va('center') + self.offsetText.set_ha(align) + self.offsetText.draw(renderer) + + # Draw grid lines + if len(xyz0) > 0: + # Grid points at end of one plane + xyz1 = copy.deepcopy(xyz0) + newindex = (index + 1) % 3 + newval = get_flip_min_max(xyz1[0], newindex, mins, maxs) + for i in range(len(majorLocs)): + xyz1[i][newindex] = newval + + # Grid points at end of the other plane + xyz2 = copy.deepcopy(xyz0) + newindex = (index + 2) % 3 + newval = get_flip_min_max(xyz2[0], newindex, mins, maxs) + for i in range(len(majorLocs)): + xyz2[i][newindex] = newval + + lines = list(zip(xyz1, xyz0, xyz2)) + if self.axes._draw_grid: + self.gridlines.set_segments(lines) + self.gridlines.set_color([info['grid']['color']] * len(lines)) + self.gridlines.set_linewidth( + [info['grid']['linewidth']] * len(lines)) + self.gridlines.set_linestyle( + [info['grid']['linestyle']] * len(lines)) + self.gridlines.draw(renderer, project=True) + + # Draw ticks + tickdir = info['tickdir'] + tickdelta = deltas[tickdir] + if highs[tickdir]: + ticksign = 1 + else: + ticksign = -1 + + for tick, loc, label in zip(majorTicks, majorLocs, majorLabels): + if tick is None: + continue + + # Get tick line positions + pos = copy.copy(edgep1) + pos[index] = loc + pos[tickdir] = ( + edgep1[tickdir] + + info['tick']['outward_factor'] * ticksign * tickdelta) + x1, y1, z1 = proj3d.proj_transform(pos[0], pos[1], pos[2], + renderer.M) + pos[tickdir] = ( + edgep1[tickdir] + - info['tick']['inward_factor'] * ticksign * tickdelta) + x2, y2, z2 = proj3d.proj_transform(pos[0], pos[1], pos[2], + renderer.M) + + # Get position of label + default_offset = 8. # A rough estimate + labeldeltas = ( + (tick.get_pad() + default_offset) * deltas_per_point * deltas) + + axmask = [True, True, True] + axmask[index] = False + pos[tickdir] = edgep1[tickdir] + pos = move_from_center(pos, centers, labeldeltas, axmask) + lx, ly, lz = proj3d.proj_transform(pos[0], pos[1], pos[2], + renderer.M) + + tick_update_position(tick, (x1, x2), (y1, y2), (lx, ly)) + tick.tick1line.set_linewidth(info['tick']['linewidth']) + tick.tick1line.set_color(info['tick']['color']) + tick.set_label1(label) + tick.set_label2(label) + tick.draw(renderer) + + renderer.close_group('axis3d') + self.stale = False + + def get_view_interval(self): + """return the Interval instance for this 3d axis view limits""" + return self.v_interval + + def set_view_interval(self, vmin, vmax, ignore=False): + if ignore: + self.v_interval = vmin, vmax + else: + Vmin, Vmax = self.get_view_interval() + self.v_interval = min(vmin, Vmin), max(vmax, Vmax) + + # TODO: Get this to work properly when mplot3d supports + # the transforms framework. + def get_tightbbox(self, renderer) : + # Currently returns None so that Axis.get_tightbbox + # doesn't return junk info. + return None + +# Use classes to look at different data limits + +class XAxis(Axis): + def get_data_interval(self): + 'return the Interval instance for this axis data limits' + return self.axes.xy_dataLim.intervalx + +class YAxis(Axis): + def get_data_interval(self): + 'return the Interval instance for this axis data limits' + return self.axes.xy_dataLim.intervaly + +class ZAxis(Axis): + def get_data_interval(self): + 'return the Interval instance for this axis data limits' + return self.axes.zz_dataLim.intervalx diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/proj3d.py b/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/proj3d.py new file mode 100644 index 0000000000..a084e7f36a --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/proj3d.py @@ -0,0 +1,203 @@ +# 3dproj.py +# +""" +Various transforms used for by the 3D code +""" +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six +from six.moves import zip + +import numpy as np +import numpy.linalg as linalg + + + +def line2d(p0, p1): + """ + Return 2D equation of line in the form ax+by+c = 0 + """ + # x + x1 = 0 + x0, y0 = p0[:2] + x1, y1 = p1[:2] + # + if x0 == x1: + a = -1 + b = 0 + c = x1 + elif y0 == y1: + a = 0 + b = 1 + c = -y1 + else: + a = (y0-y1) + b = (x0-x1) + c = (x0*y1 - x1*y0) + return a, b, c + +def line2d_dist(l, p): + """ + Distance from line to point + line is a tuple of coefficients a,b,c + """ + a, b, c = l + x0, y0 = p + return abs((a*x0 + b*y0 + c)/np.sqrt(a**2+b**2)) + + +def line2d_seg_dist(p1, p2, p0): + """distance(s) from line defined by p1 - p2 to point(s) p0 + + p0[0] = x(s) + p0[1] = y(s) + + intersection point p = p1 + u*(p2-p1) + and intersection point lies within segment if u is between 0 and 1 + """ + + x21 = p2[0] - p1[0] + y21 = p2[1] - p1[1] + x01 = np.asarray(p0[0]) - p1[0] + y01 = np.asarray(p0[1]) - p1[1] + + u = (x01*x21 + y01*y21) / (x21**2 + y21**2) + u = np.clip(u, 0, 1) + d = np.sqrt((x01 - u*x21)**2 + (y01 - u*y21)**2) + + return d + + +def mod(v): + """3d vector length""" + return np.sqrt(v[0]**2+v[1]**2+v[2]**2) + +def world_transformation(xmin, xmax, + ymin, ymax, + zmin, zmax): + dx, dy, dz = (xmax-xmin), (ymax-ymin), (zmax-zmin) + return np.array([ + [1.0/dx,0,0,-xmin/dx], + [0,1.0/dy,0,-ymin/dy], + [0,0,1.0/dz,-zmin/dz], + [0,0,0,1.0]]) + + +def view_transformation(E, R, V): + n = (E - R) + ## new +# n /= mod(n) +# u = np.cross(V,n) +# u /= mod(u) +# v = np.cross(n,u) +# Mr = np.diag([1.]*4) +# Mt = np.diag([1.]*4) +# Mr[:3,:3] = u,v,n +# Mt[:3,-1] = -E + ## end new + + ## old + n = n / mod(n) + u = np.cross(V, n) + u = u / mod(u) + v = np.cross(n, u) + Mr = [[u[0],u[1],u[2],0], + [v[0],v[1],v[2],0], + [n[0],n[1],n[2],0], + [0, 0, 0, 1], + ] + # + Mt = [[1, 0, 0, -E[0]], + [0, 1, 0, -E[1]], + [0, 0, 1, -E[2]], + [0, 0, 0, 1]] + ## end old + + return np.dot(Mr, Mt) + +def persp_transformation(zfront, zback): + a = (zfront+zback)/(zfront-zback) + b = -2*(zfront*zback)/(zfront-zback) + return np.array([[1,0,0,0], + [0,1,0,0], + [0,0,a,b], + [0,0,-1,0] + ]) + +def ortho_transformation(zfront, zback): + # note: w component in the resulting vector will be (zback-zfront), not 1 + a = -(zfront + zback) + b = -(zfront - zback) + return np.array([[2,0,0,0], + [0,2,0,0], + [0,0,-2,0], + [0,0,a,b] + ]) + +def proj_transform_vec(vec, M): + vecw = np.dot(M, vec) + w = vecw[3] + # clip here.. + txs, tys, tzs = vecw[0]/w, vecw[1]/w, vecw[2]/w + return txs, tys, tzs + +def proj_transform_vec_clip(vec, M): + vecw = np.dot(M, vec) + w = vecw[3] + # clip here. + txs, tys, tzs = vecw[0] / w, vecw[1] / w, vecw[2] / w + tis = (0 <= vecw[0]) & (vecw[0] <= 1) & (0 <= vecw[1]) & (vecw[1] <= 1) + if np.any(tis): + tis = vecw[1] < 1 + return txs, tys, tzs, tis + +def inv_transform(xs, ys, zs, M): + iM = linalg.inv(M) + vec = vec_pad_ones(xs, ys, zs) + vecr = np.dot(iM, vec) + try: + vecr = vecr/vecr[3] + except OverflowError: + pass + return vecr[0], vecr[1], vecr[2] + +def vec_pad_ones(xs, ys, zs): + return np.array([xs, ys, zs, np.ones_like(xs)]) + +def proj_transform(xs, ys, zs, M): + """ + Transform the points by the projection matrix + """ + vec = vec_pad_ones(xs, ys, zs) + return proj_transform_vec(vec, M) + +def proj_transform_clip(xs, ys, zs, M): + """ + Transform the points by the projection matrix + and return the clipping result + returns txs,tys,tzs,tis + """ + vec = vec_pad_ones(xs, ys, zs) + return proj_transform_vec_clip(vec, M) +transform = proj_transform + +def proj_points(points, M): + return np.column_stack(proj_trans_points(points, M)) + +def proj_trans_points(points, M): + xs, ys, zs = zip(*points) + return proj_transform(xs, ys, zs, M) + +def proj_trans_clip_points(points, M): + xs, ys, zs = zip(*points) + return proj_transform_clip(xs, ys, zs, M) + + +def rot_x(V, alpha): + cosa, sina = np.cos(alpha), np.sin(alpha) + M1 = np.array([[1,0,0,0], + [0,cosa,-sina,0], + [0,sina,cosa,0], + [0,0,0,1]]) + + return np.dot(M1, V) |