HW8 Complete

This commit is contained in:
2020-03-25 22:08:20 -06:00
parent 117ffcb5fb
commit 02d74cfd9a
4003 changed files with 1124519 additions and 1 deletions

View File

@@ -0,0 +1,12 @@
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, which provide'
' the same functionality',
obj_type='module')

View File

@@ -0,0 +1,6 @@
from matplotlib.offsetbox import AnchoredOffsetbox, AuxTransformBox, VPacker,\
TextArea, AnchoredText, DrawingArea, AnnotationBbox
from mpl_toolkits.axes_grid1.anchored_artists import \
AnchoredDrawingArea, AnchoredAuxTransformBox, \
AnchoredEllipse, AnchoredSizeBar

View File

@@ -0,0 +1 @@
from mpl_toolkits.axisartist.angle_helper import *

View File

@@ -0,0 +1,5 @@
from mpl_toolkits.axes_grid1.axes_divider import (
AxesDivider, AxesLocator, Divider, SubplotDivider, locatable_axes_factory,
make_axes_locatable)
from mpl_toolkits.axisartist.axes_divider import LocatableAxes
from mpl_toolkits.axisartist.axislines import Axes

View File

@@ -0,0 +1,3 @@
from mpl_toolkits.axisartist.axes_divider import LocatableAxes
from mpl_toolkits.axisartist.axes_grid import (
AxesGrid, CbarAxes, Grid, ImageGrid)

View File

@@ -0,0 +1,7 @@
from mpl_toolkits.axes_grid1.axes_rgb import (
make_rgb_axes, imshow_rgb, RGBAxesBase)
from mpl_toolkits.axisartist.axislines import Axes
class RGBAxes(RGBAxesBase):
_defaultAxesClass = Axes

View File

@@ -0,0 +1 @@
from mpl_toolkits.axes_grid1.axes_size import *

View File

@@ -0,0 +1 @@
from mpl_toolkits.axisartist.axis_artist import *

View File

@@ -0,0 +1 @@
from mpl_toolkits.axisartist.axisline_style import *

View File

@@ -0,0 +1 @@
from mpl_toolkits.axisartist.axislines import *

View File

@@ -0,0 +1 @@
from mpl_toolkits.axisartist.clip_path import *

View File

@@ -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
)

View File

@@ -0,0 +1 @@
from mpl_toolkits.axisartist.floating_axes import *

View File

@@ -0,0 +1 @@
from mpl_toolkits.axisartist.grid_finder import *

View File

@@ -0,0 +1 @@
from mpl_toolkits.axisartist.grid_helper_curvelinear import *

View File

@@ -0,0 +1,4 @@
from mpl_toolkits.axes_grid1.inset_locator import InsetPosition, \
AnchoredSizeLocator, \
AnchoredZoomLocator, BboxPatch, BboxConnector, BboxConnectorPatch, \
inset_axes, zoomed_inset_axes, mark_inset

View File

@@ -0,0 +1,14 @@
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 mpl_toolkits.axisartist.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)

View File

@@ -0,0 +1,7 @@
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

View File

@@ -0,0 +1,590 @@
from matplotlib import docstring, transforms
from matplotlib.offsetbox import (AnchoredOffsetbox, AuxTransformBox,
DrawingArea, TextArea, VPacker)
from matplotlib.patches import (Rectangle, Ellipse, ArrowStyle,
FancyArrowPatch, PathPatch)
from matplotlib.text import TextPath
__all__ = ['AnchoredDrawingArea', 'AnchoredAuxTransformBox',
'AnchoredEllipse', 'AnchoredSizeBar', 'AnchoredDirectionArrows']
class AnchoredDrawingArea(AnchoredOffsetbox):
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='upper right', 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().__init__(
loc, pad=pad, borderpad=borderpad, child=self.da, prop=None,
frameon=frameon, **kwargs
)
class AnchoredAuxTransformBox(AnchoredOffsetbox):
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='upper left')
>>> 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):
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):
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)
class AnchoredDirectionArrows(AnchoredOffsetbox):
def __init__(self, transform, label_x, label_y, length=0.15,
fontsize=0.08, loc=2, angle=0, aspect_ratio=1, pad=0.4,
borderpad=0.4, frameon=False, color='w', alpha=1,
sep_x=0.01, sep_y=0, fontproperties=None, back_length=0.15,
head_width=10, head_length=15, tail_width=2,
text_props=None, arrow_props=None,
**kwargs):
"""
Draw two perpendicular arrows to indicate directions.
Parameters
----------
transform : `matplotlib.transforms.Transform`
The transformation object for the coordinate system in use, i.e.,
:attr:`matplotlib.axes.Axes.transAxes`.
label_x, label_y : string
Label text for the x and y arrows
length : int or float, optional
Length of the arrow, given in coordinates of
*transform*.
Defaults to 0.15.
fontsize : int, optional
Size of label strings, given in coordinates of *transform*.
Defaults to 0.08.
loc : int, optional
Location of the direction arrows. 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
Defaults to 2.
angle : int or float, optional
The angle of the arrows in degrees.
Defaults to 0.
aspect_ratio : int or float, optional
The ratio of the length of arrow_x and arrow_y.
Negative numbers can be used to change the direction.
Defaults to 1.
pad : int or float, optional
Padding around the labels and arrows, 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.4.
frameon : bool, optional
If True, draw a box around the arrows and labels.
Defaults to False.
color : str, optional
Color for the arrows and labels.
Defaults to white.
alpha : int or float, optional
Alpha values of the arrows and labels
Defaults to 1.
sep_x, sep_y : int or float, optional
Separation between the arrows and labels in coordinates of
*transform*. Defaults to 0.01 and 0.
fontproperties : `matplotlib.font_manager.FontProperties`, optional
Font properties for the label text.
back_length : float, optional
Fraction of the arrow behind the arrow crossing.
Defaults to 0.15.
head_width : int or float, optional
Width of arrow head, sent to ArrowStyle.
Defaults to 10.
head_length : int or float, optional
Length of arrow head, sent to ArrowStyle.
Defaults to 15.
tail_width : int or float, optional
Width of arrow tail, sent to ArrowStyle.
Defaults to 2.
text_props, arrow_props : dict
Properties of the text and arrows, passed to
:class:`matplotlib.text.TextPath` and
`matplotlib.patches.FancyArrowPatch`
**kwargs
Keyworded arguments to pass to
:class:`matplotlib.offsetbox.AnchoredOffsetbox`.
Attributes
----------
arrow_x, arrow_y : `matplotlib.patches.FancyArrowPatch`
Arrow x and y
text_path_x, text_path_y : `matplotlib.text.TextPath`
Path for arrow labels
p_x, p_y : `matplotlib.patches.PathPatch`
Patch for arrow labels
box : `matplotlib.offsetbox.AuxTransformBox`
Container for the arrows and labels.
Notes
-----
If *prop* is passed as a keyword 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 (
... AnchoredDirectionArrows)
>>> fig, ax = plt.subplots()
>>> ax.imshow(np.random.random((10,10)))
>>> arrows = AnchoredDirectionArrows(ax.transAxes, '111', '110')
>>> ax.add_artist(arrows)
>>> fig.show()
Using several of the optional parameters, creating downward pointing
arrow and high contrast text labels.
>>> import matplotlib.font_manager as fm
>>> fontprops = fm.FontProperties(family='monospace')
>>> arrows = AnchoredDirectionArrows(ax.transAxes, 'East', 'South',
... loc='lower left', color='k',
... aspect_ratio=-1, sep_x=0.02,
... sep_y=-0.01,
... text_props={'ec':'w', 'fc':'k'},
... fontproperties=fontprops)
"""
if arrow_props is None:
arrow_props = {}
if text_props is None:
text_props = {}
arrowstyle = ArrowStyle("Simple",
head_width=head_width,
head_length=head_length,
tail_width=tail_width)
if fontproperties is None and 'prop' in kwargs:
fontproperties = kwargs.pop('prop')
if 'color' not in arrow_props:
arrow_props['color'] = color
if 'alpha' not in arrow_props:
arrow_props['alpha'] = alpha
if 'color' not in text_props:
text_props['color'] = color
if 'alpha' not in text_props:
text_props['alpha'] = alpha
t_start = transform
t_end = t_start + transforms.Affine2D().rotate_deg(angle)
self.box = AuxTransformBox(t_end)
length_x = length
length_y = length*aspect_ratio
self.arrow_x = FancyArrowPatch(
(0, back_length*length_y),
(length_x, back_length*length_y),
arrowstyle=arrowstyle,
shrinkA=0.0,
shrinkB=0.0,
**arrow_props)
self.arrow_y = FancyArrowPatch(
(back_length*length_x, 0),
(back_length*length_x, length_y),
arrowstyle=arrowstyle,
shrinkA=0.0,
shrinkB=0.0,
**arrow_props)
self.box.add_artist(self.arrow_x)
self.box.add_artist(self.arrow_y)
text_path_x = TextPath((
length_x+sep_x, back_length*length_y+sep_y), label_x,
size=fontsize, prop=fontproperties)
self.p_x = PathPatch(text_path_x, transform=t_start, **text_props)
self.box.add_artist(self.p_x)
text_path_y = TextPath((
length_x*back_length+sep_x, length_y*(1-back_length)+sep_y),
label_y, size=fontsize, prop=fontproperties)
self.p_y = PathPatch(text_path_y, **text_props)
self.box.add_artist(self.p_y)
AnchoredOffsetbox.__init__(self, loc, pad=pad, borderpad=borderpad,
child=self.box,
frameon=frameon, **kwargs)

View File

@@ -0,0 +1,895 @@
"""
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 matplotlib import cbook
from matplotlib.axes import SubplotBase
from matplotlib.gridspec import SubplotSpec, GridSpec
import matplotlib.transforms as mtransforms
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:
cbook._check_in_list(["left", "right", "bottom", "top"],
position=position)
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
class SubplotDivider(Divider):
"""
The Divider class whose rectangle area is specified as a subplot geometry.
"""
def __init__(self, fig, *args, horizontal=None, vertical=None,
aspect=None, anchor='C'):
"""
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(f'Illegal argument(s) to subplot: {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
Divider.__init__(self, fig, pos, horizontal or [], vertical or [],
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, *, axes_class=None, **kwargs):
axes = self._axes
if axes_class is None:
if isinstance(axes, SubplotBase):
axes_class = axes._axes_class
else:
axes_class = type(axes)
return axes_class(axes.get_figure(), axes.get_position(original=True),
**kwargs)
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:
cbook._check_in_list(["left", "right", "bottom", "top"],
position=position)
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):
"""
Create a new `~mpl_toolkits.axes_grid.axes_divider.AxesLocator` for
the 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):
"""
Create a new `~mpl_toolkits.axes_grid.axes_divider.AxesLocator` for
the 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)
@cbook.deprecated('3.0',
addendum=' There is no alternative. Deriving from '
'matplotlib.axes.Axes provides this functionality '
'already.')
class LocatableAxesBase(object):
pass
@cbook.deprecated('3.0',
addendum=' There is no alternative. Classes derived from '
'matplotlib.axes.Axes provide this functionality '
'already.')
def locatable_axes_factory(axes_class):
return axes_class
def make_axes_locatable(axes):
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 .mpl_axes import Axes as _Axes
@cbook.deprecated('3.0',
alternative='mpl_toolkits.axes_grid1.mpl_axes.Axes')
class Axes(_Axes):
pass
@cbook.deprecated('3.0',
alternative='mpl_toolkits.axes_grid1.mpl_axes.Axes')
class LocatableAxes(_Axes):
pass

View File

@@ -0,0 +1,777 @@
from numbers import Number
import matplotlib.axes as maxes
import matplotlib.ticker as ticker
from matplotlib.gridspec import SubplotSpec
from .axes_divider import Size, SubplotDivider, Divider
from .colorbar import Colorbar
from .mpl_axes import Axes
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, *, locator=None, **kwargs):
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
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, Axes):
def __init__(self, *args, orientation, **kwargs):
self.orientation = orientation
self._default_label_on = True
self.locator = None
super().__init__(*args, **kwargs)
def cla(self):
super().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.
"""
_defaultAxesClass = Axes
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,
):
"""
Parameters
----------
fig : `.Figure`
The parent figure.
rect : (float, float, float, float) or int
The axes position, as a ``(left, bottom, width, height)`` tuple or
as a three-digit subplot position code (e.g., "121").
direction : {"row", "column"}, default: "row"
axes_pad : float or (float, float), default: 0.02
Padding or (horizontal padding, vertical padding) between axes, in
inches.
add_all : bool, default: True
share_all : bool, default: False
share_x : bool, default: True
share_y : bool, default: True
label_mode : {"L", "1", "all"}, default: "L"
Determines which axes will get tick labels:
- "L": All axes on the left column get vertical tick labels;
all axes on the bottom row get horizontal tick labels.
- "1": Only the bottom left axes is labelled.
- "all": all axes are labelled.
axes_class : a type that is a subclass of `matplotlib.axes.Axes`, \
default: None
"""
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
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._defaultAxesClass
axes_class_args = {}
else:
if (isinstance(axes_class, type)
and issubclass(axes_class,
self._defaultAxesClass.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, (str, Number)):
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):
"""
Return the number of rows and columns of the grid as (nrows, ncols).
"""
return self._nrows, self._ncols
def set_axes_pad(self, axes_pad):
"""
Set the padding between the axes.
Parameters
----------
axes_pad : (float, float)
The padding (horizontal pad, vertical pad) in inches.
"""
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):
"""
Return the axes padding.
Returns
-------
hpad, vpad
Padding (horizontal pad, vertical pad) in inches.
"""
return self._axes_pad
def set_aspect(self, aspect):
"""Set the aspect of the SubplotDivider."""
self._divider.set_aspect(aspect)
def get_aspect(self):
"""Return the aspect of the SubplotDivider."""
return self._divider.get_aspect()
def set_label_mode(self, mode):
"""
Define which axes have tick labels.
Parameters
----------
mode : {"L", "1", "all"}
The label mode:
- "L": All axes on the left column get vertical tick labels;
all axes on the bottom row get horizontal tick labels.
- "1": Only the bottom left axes is labelled.
- "all": all axes are labelled.
"""
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,
):
"""
Parameters
----------
fig : `.Figure`
The parent figure.
rect : (float, float, float, float) or int
The axes position, as a ``(left, bottom, width, height)`` tuple or
as a three-digit subplot position code (e.g., "121").
direction : {"row", "column"}, default: "row"
axes_pad : float or (float, float), default: 0.02
Padding or (horizontal padding, vertical padding) between axes, in
inches.
add_all : bool, default: True
share_all : bool, default: False
aspect : bool, default: True
label_mode : {"L", "1", "all"}, default: "L"
Determines which axes will get tick labels:
- "L": All axes on the left column get vertical tick labels;
all axes on the bottom row get horizontal tick labels.
- "1": Only the bottom left axes is labelled.
- "all": all axes are labelled.
cbar_mode : {"each", "single", "edge", None }, default: None
cbar_location : {"left", "right", "bottom", "top"}, default: "right"
cbar_pad : float, default: None
cbar_size : size specification (see `.Size.from_any`), default: "5%"
cbar_set_cax : bool, default: True
If True, each axes in the grid has a *cax* attribute that is bound
to associated *cbar_axes*.
axes_class : a type that is a subclass of `matplotlib.axes.Axes`, \
default: None
"""
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._defaultAxesClass
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, (str, Number)):
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

View File

@@ -0,0 +1,223 @@
import numpy as np
from .axes_divider import make_axes_locatable, Size
from .mpl_axes import Axes
def make_rgb_axes(ax, pad=0.01, axes_class=None, add_all=True):
"""
Parameters
----------
pad : float
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 = ax._axes_class
except AttributeError:
axes_class = 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, *args, pad=0, add_all=True, **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
*args
Unpacked into axes_class() init for RGB
**kwargs
Unpacked into axes_class() init for RGB, R, G, B axes
"""
try:
axes_class = kwargs.pop("axes_class", self._defaultAxesClass)
except AttributeError:
raise AttributeError(
'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.'
)
ax = axes_class(*args, **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

View File

@@ -0,0 +1,316 @@
"""
Provides 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 numbers import Number
from matplotlib import 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 isinstance(size, Number):
return Fixed(size)
elif isinstance(size, str):
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):
_get_func_map = {
"left": lambda self, axes_bbox: axes_bbox.xmin - self.xmin,
"right": lambda self, axes_bbox: self.xmax - axes_bbox.xmax,
"bottom": lambda self, axes_bbox: axes_bbox.ymin - self.ymin,
"top": lambda self, axes_bbox: self.ymax - axes_bbox.ymax,
}
def __init__(self, ax, direction):
cbook._check_in_list(self._get_func_map, direction=direction)
self._ax_list = [ax] if isinstance(ax, Axes) else ax
self._direction = direction
def __call__(self, renderer):
get_func = self._get_func_map[self._direction]
vl = [get_func(ax.get_tightbbox(renderer, call_axes_locator=False),
ax.bbox)
for ax in self._ax_list]
return max(vl)

View File

@@ -0,0 +1,794 @@
"""
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`.
"""
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.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 pyplot
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, str):
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 np.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 range(1, N-1)]
else:
return [list(zip(Y[i], X[i])) for i in range(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_facecolor(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_facecolor(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))
del self.solids
del self.dividers
col = self.ax.pcolormesh(
*args,
cmap=self.cmap, norm=self.norm, shading='flat', alpha=self.alpha)
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.
'''
X, Y = np.meshgrid([1, 2], levels)
if self.orientation == 'vertical':
xy = np.stack([X, Y], axis=-1)
else:
xy = np.stack([Y, X], axis=-1)
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)
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 = (self._boundaries[:-1]
+ self._boundaries[1:]) / 2
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)
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):
self._boundaries = (
self._uniform_y(self.cmap.N + 1) * self.cmap.N - 0.5)
self._values = np.arange(self.cmap.N, dtype=np.int16)
return
elif isinstance(self.norm, colors.BoundaryNorm):
self._boundaries = np.array(self.norm.boundaries)
self._values = (self._boundaries[:-1] + self._boundaries[1:]) / 2
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):
# 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.
mappable.autoscale_None()
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, *, fraction=0.15, shrink=1.0, aspect=20, **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')
#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 pyplot 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)
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

View File

@@ -0,0 +1,673 @@
"""
A collection of functions and objects for creating or placing inset axes.
"""
from matplotlib import cbook, docstring
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().__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().__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().__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):
# docstring inherited
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)
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_transform()
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()
if 'fill' in kwargs:
Patch.__init__(self, **kwargs)
else:
fill = bool({'fc', 'facecolor', 'color'}.intersection(kwargs))
Patch.__init__(self, fill=fill, **kwargs)
self.bbox1 = bbox1
self.bbox2 = bbox2
self.loc1 = loc1
self.loc2 = loc2
def get_path(self):
# docstring inherited
return self.connect_bbox(self.bbox1, self.bbox2,
self.loc1, self.loc2)
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):
# docstring inherited
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 = [*path1.vertices, *path2.vertices, path1.vertices[0]]
return Path(path_merged)
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='upper right',
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 :doc:`the examples
</gallery/axes_grid1/inset_locator_demo>`.
Notes
-----
The meaning of *bbox_to_anchor* and *bbox_to_transform* is interpreted
differently from that of legend. The value of bbox_to_anchor
(or the return value of its get_points method; the default is
*parent_axes.bbox*) is transformed by the bbox_transform (the default
is Identity transform) and then interpreted as points in the pixel
coordinate (which is dpi dependent).
Thus, following three calls are identical and creates an inset axes
with respect to the *parent_axes*::
axins = inset_axes(parent_axes, "30%%", "40%%")
axins = inset_axes(parent_axes, "30%%", "40%%",
bbox_to_anchor=parent_axes.bbox)
axins = inset_axes(parent_axes, "30%%", "40%%",
bbox_to_anchor=(0, 0, 1, 1),
bbox_transform=parent_axes.transAxes)
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,
a tuple of (0, 0, 1, 1) is used if *bbox_transform* is set
to *parent_axes.transAxes* or *parent_axes.figure.transFigure*.
Otherwise, *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,
unless *bbox_transform* is set, the units of the bounding box
are interpreted in the pixel coordinate. When using *bbox_to_anchor*
with tuple, 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. The value
of *bbox_to_anchor* (or the return value of its get_points method)
is transformed by the *bbox_transform* and then interpreted
as points in the pixel coordinate (which is dpi dependent).
You may provide *bbox_to_anchor* in some normalized coordinate,
and give an appropriate transform (e.g., *parent_axes.transAxes*).
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:
cbook._warn_external("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='upper right',
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
:doc:`the examples </gallery/axes_grid1/inset_locator_demo2>`.
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)
if 'fill' in kwargs:
pp = BboxPatch(rect, **kwargs)
else:
fill = bool({'fc', 'facecolor', 'color'}.intersection(kwargs))
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

View File

@@ -0,0 +1,134 @@
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, *args, **kwargs):
for m in self._objects:
m(*args, **kwargs)
class Axes(maxes.Axes):
class AxisDict(dict):
def __init__(self, axes):
self.axes = axes
super().__init__()
def __getitem__(self, k):
if isinstance(k, tuple):
r = SimpleChainedObjects(
[super(Axes.AxisDict, self).__getitem__(k1) for k1 in k])
# super() within a list comprehension needs explicit args
return r
elif isinstance(k, slice):
if k.start is None and k.stop is None and k.step is None:
return SimpleChainedObjects(list(self.values()))
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_axis_artists(self, axes=None):
if axes is None:
axes = self
self._axislines = self.AxisDict(self)
self._axislines.update(
bottom=SimpleAxisArtist(self.xaxis, 1, self.spines["bottom"]),
top=SimpleAxisArtist(self.xaxis, 2, self.spines["top"]),
left=SimpleAxisArtist(self.yaxis, 1, self.spines["left"]),
right=SimpleAxisArtist(self.yaxis, 2, self.spines["right"]))
@property
def axis(self):
return self._axislines
def cla(self):
super().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(
f"axis must be instance of XAxis or YAxis, but got {axis}")
Artist.__init__(self)
@property
def major_ticks(self):
tickline = "tick%dline" % self._axisnum
return SimpleChainedObjects([getattr(tick, tickline)
for tick in self._axis.get_major_ticks()])
@property
def major_ticklabels(self):
label = "label%d" % self._axisnum
return SimpleChainedObjects([getattr(tick, label)
for tick in self._axis.get_major_ticks()])
@property
def label(self):
return self._axis.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)

View File

@@ -0,0 +1,419 @@
import functools
from matplotlib import artist as martist, cbook, transforms as mtransforms
from matplotlib.axes import subplot_class_factory
from matplotlib.transforms import Bbox
from .mpl_axes import Axes
import numpy as np
class ParasiteAxesBase:
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, **kwargs):
self._parent_axes = parent_axes
kwargs["frameon"] = False
super().__init__(parent_axes.figure, parent_axes._position, **kwargs)
def cla(self):
super().cla()
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)
def pick(self, mouseevent):
# This most likely goes to Artist.pick (depending on axes_class given
# to the factory), which only handles pick events registered on the
# axes associated with each child:
super().pick(mouseevent)
# But parasite axes are additionally given pick events from their host
# axes (cf. HostAxesBase.pick), which we handle here:
for a in self.get_children():
if (hasattr(mouseevent.inaxes, "parasites")
and self in mouseevent.inaxes.parasites):
a.pick(mouseevent)
@functools.lru_cache(None)
def parasite_axes_class_factory(axes_class=None):
if axes_class is None:
axes_class = Axes
return type("%sParasite" % axes_class.__name__,
(ParasiteAxesBase, axes_class), {})
ParasiteAxes = parasite_axes_class_factory()
class ParasiteAxesAuxTransBase:
def __init__(self, parent_axes, aux_transform, viewlim_mode=None,
**kwargs):
self.transAux = aux_transform
self.set_viewlim_mode(viewlim_mode)
super().__init__(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):
cbook._check_in_list([None, "equal", "transform"], mode=mode)
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:
cbook._check_in_list([None, "equal", "transform"], mode=mode)
def _pcolor(self, super_pcolor, *XYC, **kwargs):
if len(XYC) == 1:
C = XYC[0]
ny, nx = C.shape
gx = np.arange(-0.5, nx)
gy = np.arange(-0.5, ny)
X, Y = np.meshgrid(gx, gy)
else:
X, Y, C = XYC
if "transform" in kwargs:
mesh = super_pcolor(X, Y, C, **kwargs)
else:
orig_shape = X.shape
xyt = np.column_stack([X.flat, Y.flat])
wxy = self.transAux.transform(xyt)
gx = wxy[:, 0].reshape(orig_shape)
gy = wxy[:, 1].reshape(orig_shape)
mesh = super_pcolor(gx, gy, C, **kwargs)
mesh.set_transform(self._parent_axes.transData)
return mesh
def pcolormesh(self, *XYC, **kwargs):
return self._pcolor(super().pcolormesh, *XYC, **kwargs)
def pcolor(self, *XYC, **kwargs):
return self._pcolor(super().pcolor, *XYC, **kwargs)
def _contour(self, super_contour, *XYCL, **kwargs):
if len(XYCL) <= 2:
C = XYCL[0]
ny, nx = C.shape
gx = np.arange(0., nx)
gy = np.arange(0., ny)
X, Y = np.meshgrid(gx, gy)
CL = XYCL
else:
X, Y = XYCL[:2]
CL = XYCL[2:]
if "transform" in kwargs:
cont = super_contour(X, Y, *CL, **kwargs)
else:
orig_shape = X.shape
xyt = np.column_stack([X.flat, Y.flat])
wxy = self.transAux.transform(xyt)
gx = wxy[:, 0].reshape(orig_shape)
gy = wxy[:, 1].reshape(orig_shape)
cont = super_contour(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(super().contour, *XYCL, **kwargs)
def contourf(self, *XYCL, **kwargs):
return self._contour(super().contourf, *XYCL, **kwargs)
def apply_aspect(self, position=None):
self.update_viewlim()
super().apply_aspect()
@functools.lru_cache(None)
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
return type("%sParasiteAuxTrans" % parasite_axes_class.__name__,
(ParasiteAxesAuxTransBase, parasite_axes_class),
{'name': 'parasite_axes'})
ParasiteAxesAuxTrans = parasite_axes_auxtrans_class_factory(
axes_class=ParasiteAxes)
class HostAxesBase:
def __init__(self, *args, **kwargs):
self.parasites = []
super().__init__(*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
# Anything you draw in ax2 will match the ticks and grids of ax1.
self.parasites.append(ax2)
ax2._remove_method = self.parasites.remove
return ax2
def _get_legend_handles(self, legend_handler_map=None):
all_handles = super()._get_legend_handles()
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)
super().draw(renderer)
self.artists = orig_artists
self.images = orig_images
def cla(self):
for ax in self.parasites:
ax.cla()
super().cla()
def pick(self, mouseevent):
super().pick(mouseevent)
# Also pass pick events on to parasite axes and, in turn, their
# children (cf. ParasiteAxesBase.pick)
for a in self.parasites:
a.pick(mouseevent)
def twinx(self, axes_class=None):
"""
Create a twin of Axes with a shared x-axis but independent y-axis.
The y-axis of self will have ticks on the 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)
ax2._remove_method = self._remove_twinx
self.axis["right"].set_visible(False)
ax2.axis["right"].set_visible(True)
ax2.axis["left", "top", "bottom"].set_visible(False)
return ax2
def _remove_twinx(self, ax):
self.parasites.remove(ax)
self.axis["right"].set_visible(True)
self.axis["right"].toggle(ticklabels=False, label=False)
def twiny(self, axes_class=None):
"""
Create a twin of Axes with a shared y-axis but independent x-axis.
The x-axis of self will have ticks on the 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)
ax2._remove_method = self._remove_twiny
self.axis["top"].set_visible(False)
ax2.axis["top"].set_visible(True)
ax2.axis["left", "right", "bottom"].set_visible(False)
return ax2
def _remove_twiny(self, ax):
self.parasites.remove(ax)
self.axis["top"].set_visible(True)
self.axis["top"].toggle(ticklabels=False, label=False)
def twin(self, aux_trans=None, axes_class=None):
"""
Create a twin of Axes with no shared axis.
While self will have ticks on the left and bottom axis, the returned
axes will have ticks on the top and right axis.
"""
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 = self.parasites.remove
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,
bbox_extra_artists=None):
bbs = [ax.get_tightbbox(renderer, call_axes_locator=call_axes_locator)
for ax in self.parasites]
bbs.append(super().get_tightbbox(renderer,
call_axes_locator=call_axes_locator,
bbox_extra_artists=bbox_extra_artists))
return Bbox.union([b for b in bbs if b.width != 0 or b.height != 0])
@functools.lru_cache(None)
def host_axes_class_factory(axes_class=None):
if axes_class is None:
axes_class = Axes
def _get_base_axes(self):
return axes_class
return type("%sHostAxes" % axes_class.__name__,
(HostAxesBase, axes_class),
{'_get_base_axes': _get_base_axes})
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, axes_class=None, figure=None, **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
host_axes_class = host_axes_class_factory(axes_class)
if figure is None:
figure = plt.gcf()
ax = host_axes_class(figure, *args, **kwargs)
figure.add_axes(ax)
plt.draw_if_interactive()
return ax
def host_subplot(*args, axes_class=None, figure=None, **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
host_subplot_class = host_subplot_class_factory(axes_class)
if figure is None:
figure = plt.gcf()
ax = host_subplot_class(figure, *args, **kwargs)
figure.add_subplot(ax)
plt.draw_if_interactive()
return ax

View File

@@ -0,0 +1,21 @@
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)

View File

@@ -0,0 +1,401 @@
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 = np.sign(values)
signs = ["-" if v < 0 else "" for v in 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
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 super().__call__(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.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

View File

@@ -0,0 +1,19 @@
from matplotlib import cbook
from mpl_toolkits.axes_grid1.axes_divider import (
Divider, AxesLocator, SubplotDivider, AxesDivider, locatable_axes_factory,
make_axes_locatable)
from mpl_toolkits.axisartist.axislines import Axes as _Axes
@cbook.deprecated('3.0',
alternative='mpl_toolkits.axisartist.axislines.Axes')
class Axes(_Axes):
pass
@cbook.deprecated('3.0',
alternative='mpl_toolkits.axisartist.axislines.Axes')
class LocatableAxes(_Axes):
pass

View File

@@ -0,0 +1,26 @@
import mpl_toolkits.axes_grid1.axes_grid as axes_grid_orig
from .axislines import Axes
class CbarAxes(axes_grid_orig.CbarAxesBase, Axes):
def __init__(self, *args, orientation, **kwargs):
self.orientation = orientation
self._default_label_on = False
self.locator = None
super().__init__(*args, **kwargs)
def cla(self):
super().cla()
self._config_axes()
class Grid(axes_grid_orig.Grid):
_defaultAxesClass = Axes
class ImageGrid(axes_grid_orig.ImageGrid):
_defaultAxesClass = Axes
_defaultCbarAxesClass = CbarAxes
AxesGrid = ImageGrid

View File

@@ -0,0 +1,8 @@
from mpl_toolkits.axes_grid1.axes_rgb import (
make_rgb_axes, imshow_rgb, RGBAxesBase)
from .axislines import Axes
class RGBAxes(RGBAxesBase):
_defaultAxesClass = Axes

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,157 @@
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()
extended_path = self._extend_path(path_in_disp,
mutation_size=mutation_size)
self._path_original = extended_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().__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().__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

View File

@@ -0,0 +1,675 @@
"""
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.
"""
import numpy as np
from matplotlib import cbook, rcParams
import matplotlib.artist as martist
import matplotlib.axes as maxes
from matplotlib.path import Path
from mpl_toolkits.axes_grid1 import mpl_axes
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().__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().__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().__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()
majorLabels = major.formatter.format_ticks(majorLocs)
minor = self.axis.minor
minorLocs = minor.locator()
minorLabels = minor.formatter.format_ticks(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)
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().__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
p = (axes.transData + axes.transAxes.inverted()).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)
"""
if self.nth_coord == 0:
angle = 0
else:
angle = 90
_verts = [0.5, 0.5]
fixed_coord = 1-self.nth_coord
p = (axes.transData + axes.transAxes.inverted()).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()
majorLabels = major.formatter.format_ticks(majorLocs)
minor = self.axis.minor
minorLocs = minor.locator()
minorLabels = minor.formatter.format_ticks(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
and 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().__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().__init__()
self.axes = axes
def new_fixed_axis(self, loc,
nth_coord=None,
axis_direction=None,
offset=None,
axes=None,
):
if axes is None:
cbook._warn_external(
"'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:
cbook._warn_external(
"'new_floating_axis' explicitly requires the axes keyword.")
axes = self.axes
_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 which in ["both", "major"]:
locs.extend(self.axes.xaxis.major.locator())
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:
locs.extend(self.axes.yaxis.major.locator())
if self.axes.yaxis._gridOnMinor:
locs.extend(self.axes.yaxis.minor.locator())
for y in locs:
gridlines.append([[x1, x2], [y, y]])
return gridlines
@cbook.deprecated("3.1")
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, *args, **kwargs):
for m in self._objects:
m(*args, **kwargs)
class Axes(maxes.Axes):
@cbook.deprecated("3.1")
class AxisDict(dict):
def __init__(self, axes):
self.axes = axes
super().__init__()
def __getitem__(self, k):
if isinstance(k, tuple):
return SimpleChainedObjects(
[dict.__getitem__(self, k1) for k1 in k])
elif isinstance(k, slice):
if k == slice(None):
return SimpleChainedObjects(list(self.values()))
else:
raise ValueError("Unsupported slice")
else:
return dict.__getitem__(self, k)
def __call__(self, *args, **kwargs):
return maxes.Axes.axis(self.axes, *args, **kwargs)
def __init__(self, *args, grid_helper=None, **kwargs):
self._axisline_on = True
self._grid_helper = (grid_helper if grid_helper
else GridHelperRectlinear(self))
super().__init__(*args, **kwargs)
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_artists(self, axes=None):
if axes is None:
axes = self
self._axislines = mpl_axes.Axes.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.
self.gridlines = self.new_gridlines(grid_helper)
def cla(self):
# gridlines need to b created before cla() since cla calls grid()
self._init_gridlines()
super().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().grid(b, which=which, axis=axis, **kwargs)
if not self._axisline_on:
return
if b is None:
b = (self.axes.xaxis._gridOnMinor
or self.axes.xaxis._gridOnMajor
or self.axes.yaxis._gridOnMinor
or self.axes.yaxis._gridOnMajor)
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 = [*self._axislines.values(), self.gridlines]
else:
children = []
children.extend(super().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
Subplot = maxes.subplot_class_factory(Axes)
class AxesZero(Axes):
def _init_axis_artists(self):
super()._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)

View File

@@ -0,0 +1,118 @@
import numpy as np
from math import degrees
from matplotlib import cbook
import math
def atan2(dy, dx):
if dx == 0 and dy == 0:
cbook._warn_external("dx and dy are 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 = []
xsign = 1 if xdir else -1
ysign = 1 if ydir else -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:]]))
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)
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]

View File

@@ -0,0 +1,463 @@
"""
An experimental support for curvilinear grid.
"""
import functools
# TODO :
# see if tick_iterator method can be simplified by reusing the parent method.
import numpy as np
from matplotlib.transforms import IdentityTransform
from . import grid_helper_curvelinear
from .axis_artist import AxisArtist
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().__init__(grid_helper, nth_coord, value, axis_direction=side)
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.full_like(yy0, self.value)
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.full_like(xx0, self.value)
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
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, d2 = np.rad2deg([d, d2])
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)
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().__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 = np.full_like(xx, y0)
yy1 = np.full_like(xx, y1)
yy = np.linspace(y0, y1, 100)
xx0 = np.full_like(yy, x0)
xx1 = np.full_like(yy, 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, *args, **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, *args, **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):
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)
@functools.lru_cache(None)
def floatingaxes_class_factory(axes_class):
return type("Floating %s" % axes_class.__name__,
(FloatingAxesBase, axes_class),
{'_axes_class_floating': axes_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)

View File

@@ -0,0 +1,322 @@
import numpy as np
import matplotlib.ticker as mticker
from matplotlib.transforms import Bbox, Transform
from .clip_path import clip_line_to_rect
# 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().__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 = {
"values": [],
"levels": [],
"tick_levels": dict(left=[], bottom=[], right=[], top=[]),
"tick_locs": dict(left=[], bottom=[], right=[], top=[]),
"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):
ll1 = np.column_stack([x, y])
ll2 = aux_trans.transform(ll1)
lon, lat = ll2[:, 0], ll2[:, 1]
return lon, lat
def inv_transform_xy(x, y):
ll1 = np.column_stack([x, y])
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().__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 <= 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]
return self._fmt.format_ticks(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().__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

View File

@@ -0,0 +1,431 @@
"""
An experimental support for curvilinear grid.
"""
from itertools import chain
import numpy as np
from matplotlib.path import Path
from matplotlib.transforms import Affine2D, IdentityTransform
from .axislines import AxisArtistHelper, GridHelperBase
from .axis_artist import AxisArtist
from .grid_finder import GridFinder
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().__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)
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().__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
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.full_like(yy0, 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.full_like(xx0, 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
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, d2 = np.rad2deg([d, d2])
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().__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
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:
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, ""

View File

@@ -0,0 +1,15 @@
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)

View File

@@ -0,0 +1 @@
from .axes3d import Axes3D

View File

@@ -0,0 +1,854 @@
# 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.
"""
import math
import numpy as np
from matplotlib import (
artist, cbook, colors as mcolors, lines, text as mtext, path as mpath)
from matplotlib.collections import (
LineCollection, PolyCollection, PatchCollection, PathCollection)
from matplotlib.colors import Normalize
from matplotlib.patches import Patch
from . import proj3d
def _norm_angle(a):
"""Return the given angle normalized to -180 < *a* <= 180 degrees."""
a = (a + 360) % 360
if a > 180:
a = a - 360
return a
@cbook.deprecated("3.1")
def norm_angle(a):
"""Return the given angle normalized to -180 < *a* <= 180 degrees."""
return _norm_angle(a)
def _norm_text_angle(a):
"""Return the given angle normalized to -90 < *a* <= 90 degrees."""
a = (a + 180) % 180
if a > 90:
a = a - 180
return a
@cbook.deprecated("3.1")
def norm_text_angle(a):
"""Return the given angle normalized to -90 < *a* <= 90 degrees."""
return _norm_text_angle(a)
def get_dir_vector(zdir):
"""
Return a direction vector.
Parameters
----------
zdir : {'x', 'y', 'z', None, 3-tuple}
The direction. Possible values are:
- 'x': equivalent to (1, 0, 0)
- 'y': equivalent to (0, 1, 0)
- 'z': equivalent to (0, 0, 1)
- *None*: equivalent to (0, 0, 0)
- an iterable (x, y, z) is returned unchanged.
Returns
-------
x, y, z : array-like
The direction vector. This is either a numpy.array or *zdir* itself if
*zdir* is already a length-3 iterable.
"""
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 np.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 direction.
Parameters
----------
x, y, z
The position of the text.
text : str
The text string to display.
zdir : {'x', 'y', 'z', None, 3-tuple}
The direction of the text. See `.get_dir_vector` for a description of
the values.
Other Parameters
----------------
**kwargs
All other parameters are passed on to `~matplotlib.text.Text`.
"""
def __init__(self, x=0, y=0, z=0, text='', zdir='z', **kwargs):
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
@artist.allow_rasterization
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]
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 get_tightbbox(self, renderer):
# Overwriting the 2d Text behavior which is not valid for 3d.
# For now, just return None to exclude from layout calculation.
return None
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 = np.full_like(xs, fill_value=float(zs))
except TypeError:
pass
self._verts3d = juggle_axes(xs, ys, zs, zdir)
self.stale = True
def set_data_3d(self, *args):
"""
Set the x, y and z data
Parameters
----------
x : array_like
The x-data to be plotted
y : array_like
The y-data to be plotted
z : array_like
The z-data to be plotted
Notes
-----
Accepts x, y, z arguments or a single array_like (x, y, z)
"""
if len(args) == 1:
self._verts3d = args[0]
else:
self._verts3d = args
self.stale = True
def get_data_3d(self):
"""
Get the current data
Returns
-------
verts3d : length-3 tuple or array_likes
The current data as a tuple or array_likes
"""
return self._verts3d
@artist.allow_rasterization
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 = np.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
@cbook.deprecated("3.1")
def path_to_3d_segment(path, zs=0, zdir='z'):
"""Convert a path to a 3D segment."""
return _path_to_3d_segment(path, zs=zs, zdir=zdir)
def _paths_to_3d_segments(paths, zs=0, zdir='z'):
"""Convert paths from a collection object to 3D segments."""
zs = np.broadcast_to(zs, len(paths))
segs = [_path_to_3d_segment(path, pathz, zdir)
for path, pathz in zip(paths, zs)]
return segs
@cbook.deprecated("3.1")
def paths_to_3d_segments(paths, zs=0, zdir='z'):
"""Convert paths from a collection object to 3D segments."""
return _paths_to_3d_segments(paths, zs=zs, zdir=zdir)
def _path_to_3d_segment_with_codes(path, zs=0, zdir='z'):
"""Convert a path to a 3D segment with path codes."""
zs = np.broadcast_to(zs, len(path))
pathsegs = path.iter_segments(simplify=False, curves=False)
seg_codes = [((x, y, z), code) for ((x, y), code), z in zip(pathsegs, zs)]
if seg_codes:
seg, codes = zip(*seg_codes)
seg3d = [juggle_axes(x, y, z, zdir) for (x, y, z) in seg]
else:
seg3d = []
codes = []
return seg3d, list(codes)
@cbook.deprecated("3.1")
def path_to_3d_segment_with_codes(path, zs=0, zdir='z'):
"""Convert a path to a 3D segment with path codes."""
return _path_to_3d_segment_with_codes(path, zs=zs, zdir=zdir)
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 = np.broadcast_to(zs, len(paths))
segments_codes = [_path_to_3d_segment_with_codes(path, pathz, zdir)
for path, pathz in zip(paths, zs)]
if segments_codes:
segments, codes = zip(*segments_codes)
else:
segments, codes = [], []
return list(segments), list(codes)
@cbook.deprecated("3.1")
def paths_to_3d_segments_with_codes(paths, zs=0, zdir='z'):
"""
Convert paths from a collection object to 3D segments with path codes.
"""
return _paths_to_3d_segments_with_codes(paths, zs=zs, zdir=zdir)
class Line3DCollection(LineCollection):
"""
A collection of 3D lines.
"""
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
@artist.allow_rasterization
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, zs=(), zdir='z', **kwargs):
Patch.__init__(self, *args, **kwargs)
self.set_3d_properties(zs, zdir)
def set_3d_properties(self, verts, zs=0, zdir='z'):
zs = np.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)
class PathPatch3D(Patch3D):
"""
3D PathPatch object.
"""
def __init__(self, path, *, zs=(), zdir='z', **kwargs):
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 []
@cbook.deprecated("3.1")
def get_patch_verts(patch):
"""Return a list of vertices for the path of a patch."""
return _get_patch_verts(patch)
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, zs=0, zdir='z', depthshade=True, **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.
"""
self._depthshade = depthshade
super().__init__(*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 = offsets.T
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, zs=0, zdir='z', depthshade=True, **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.
"""
self._depthshade = depthshade
super().__init__(*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 = offsets.T
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]))
return np.min(vzs) if vzs.size else 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).
Parameters
----------
za
The location or locations to place the patches in the collection along
the *zdir* axis. Default: 0.
zdir
The axis in which to place the patches. Default: "z".
depthshade
Whether to shade the patches to give a sense of depth. Default: *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, zsort='average', **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.
"""
super().__init__(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):
"""
Sets the calculation method for the z-order.
Parameters
----------
zsort : {'average', 'min', 'max'}
The function applied on the z-coordinates of the vertices in the
viewer's coordinate system, to determine the z-order. *True* is
deprecated and equivalent to 'average'.
"""
if zsort is True:
cbook.warn_deprecated(
"3.1", message="Passing True to mean 'average' for set_zsort "
"is deprecated and support will be removed in Matplotlib 3.3; "
"pass 'average' instead.")
zsort = 'average'
self._zsortfunc = self._zsort_functions[zsort]
self._sort_zpos = None
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('average')
self._facecolors3d = PolyCollection.get_facecolor(self)
self._edgecolors3d = PolyCollection.get_edgecolor(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)
# sort by depth (furthest drawn first)
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)
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)
def set_edgecolor(self, colors):
PolyCollection.set_edgecolor(self, colors)
self._edgecolors3d = PolyCollection.get_edgecolor(self)
def set_alpha(self, alpha):
"""
Set the alpha transparencies of the collection.
Parameters
----------
alpha : 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._facecolors3d = 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_facecolor(self):
return self._facecolors2d
def get_edgecolor(self):
return self._edgecolors2d
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 np.broadcast_to(
mcolors.to_rgba_array(c) if len(c) else [0, 0, 0, 0],
(num, 4))
@cbook.deprecated("3.1")
def get_colors(c, num):
"""Stretch the color argument to provide the required number *num*."""
return _get_colors(c, num)
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.
if len(zs) == 0:
return np.zeros((0, 4))
norm = Normalize(min(zs), max(zs))
sats = 1 - norm(zs) * 0.7
rgba = np.broadcast_to(mcolors.to_rgba_array(colors), (len(zs), 4))
return np.column_stack([rgba[:, :3], rgba[:, 3] * sats])
@cbook.deprecated("3.1")
def zalpha(colors, zs):
"""Modify the alphas of the color list according to depth."""
return _zalpha(colors, zs)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,475 @@
# axis3d.py, original mplot3d version by John Porter
# Created: 23 Sep 2005
# Parts rewritten by Reinier Heeres <reinier@heeres.eu>
import copy
import numpy as np
from matplotlib import (
artist, cbook, lines as mlines, axis as maxis, patches as mpatches,
rcParams)
from . import art3d, proj3d
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.'''
tick.label1.set_position(labelpos)
tick.label2.set_position(labelpos)
tick.tick1line.set_visible(True)
tick.tick2line.set_visible(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):
"""An Axis class for the 3D plots. """
# 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,
rotate_label=None, **kwargs):
# adir identifies which axes this is
self.adir = adir
# 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)
# data and viewing intervals for this direction
self.d_interval = d_intervalx
self.v_interval = v_intervalx
self.set_rotate_label(rotate_label)
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
@cbook.deprecated("3.1")
def get_tick_positions(self):
majorLocs = self.major.locator()
majorLabels = self.major.formatter.format_ticks(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')
@artist.allow_rasterization
def draw(self, renderer):
self.label._transform = self.axes.transData
renderer.open_group('axis3d')
ticks = self._update_ticks()
info = self._axinfo
index = info['i']
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 tick in ticks:
coord = minmax.copy()
coord[index] = tick.get_loc()
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(np.rad2deg(np.arctan2(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(np.rad2deg(np.arctan2(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(ticks)):
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(ticks)):
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 in ticks:
# Get tick line positions
pos = copy.copy(edgep1)
pos[index] = tick.get_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.draw(renderer)
renderer.close_group('axis3d')
self.stale = False
# 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
@property
def d_interval(self):
return self.get_data_interval()
@d_interval.setter
def d_interval(self, minmax):
return self.set_data_interval(*minmax)
@property
def v_interval(self):
return self.get_view_interval()
@v_interval.setter
def v_interval(self, minmax):
return self.set_view_interval(*minmax)
# Use classes to look at different data limits
class XAxis(Axis):
get_view_interval, set_view_interval = maxis._make_getset_interval(
"view", "xy_viewLim", "intervalx")
get_data_interval, set_data_interval = maxis._make_getset_interval(
"data", "xy_dataLim", "intervalx")
class YAxis(Axis):
get_view_interval, set_view_interval = maxis._make_getset_interval(
"view", "xy_viewLim", "intervaly")
get_data_interval, set_data_interval = maxis._make_getset_interval(
"data", "xy_dataLim", "intervaly")
class ZAxis(Axis):
get_view_interval, set_view_interval = maxis._make_getset_interval(
"view", "zz_viewLim", "intervalx")
get_data_interval, set_data_interval = maxis._make_getset_interval(
"data", "zz_dataLim", "intervalx")

View File

@@ -0,0 +1,239 @@
"""
Various transforms used for by the 3D code
"""
import numpy as np
import numpy.linalg as linalg
from matplotlib import cbook
@cbook.deprecated("3.1")
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
@cbook.deprecated("3.1")
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.hypot(a, b))
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.hypot(x01 - u*x21, y01 - u*y21)
return d
@cbook.deprecated("3.1")
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
"""
return _line2d_seg_dist(p1, p2, p0)
@cbook.deprecated("3.1", alternative="np.linalg.norm")
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/dx, 0, 0, -xmin/dx],
[0, 1/dy, 0, -ymin/dy],
[0, 0, 1/dz, -zmin/dz],
[0, 0, 0, 1]])
def view_transformation(E, R, V):
n = (E - R)
## new
# n /= np.linalg.norm(n)
# u = np.cross(V,n)
# u /= np.linalg.norm(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 / np.linalg.norm(n)
u = np.cross(V, n)
u = u / np.linalg.norm(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
@cbook.deprecated("3.1")
def proj_transform_vec(vec, M):
return _proj_transform_vec(vec, M)
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
@cbook.deprecated("3.1")
def proj_transform_vec_clip(vec, M):
return _proj_transform_vec_clip(vec, M)
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)])
@cbook.deprecated("3.1")
def vec_pad_ones(xs, ys, zs):
return _vec_pad_ones(xs, ys, zs)
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)
transform = proj_transform
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)
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)
@cbook.deprecated("3.1")
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)

View File

@@ -0,0 +1,11 @@
import os
# Check that the test directories exist
if not os.path.exists(os.path.join(
os.path.dirname(__file__), 'baseline_images')):
raise IOError(
'The baseline image directory does not exist. '
'This is most likely because the test data is not installed. '
'You may need to install matplotlib from source to get the '
'test data.')

Some files were not shown because too many files have changed in this diff Show More