I am done

This commit is contained in:
2024-10-30 22:14:35 +01:00
parent 720dc28c09
commit 40e2a747cf
36901 changed files with 5011519 additions and 0 deletions

View File

@ -0,0 +1,14 @@
from .axislines import Axes
from .axislines import ( # noqa: F401
AxesZero, AxisArtistHelper, AxisArtistHelperRectlinear,
GridHelperBase, GridHelperRectlinear, Subplot, SubplotZero)
from .axis_artist import AxisArtist, GridlinesCollection # noqa: F401
from .grid_helper_curvelinear import GridHelperCurveLinear # noqa: F401
from .floating_axes import FloatingAxes, FloatingSubplot # noqa: F401
from mpl_toolkits.axes_grid1.parasite_axes import (
host_axes_class_factory, parasite_axes_class_factory)
ParasiteAxes = parasite_axes_class_factory(Axes)
HostAxes = host_axes_class_factory(Axes)
SubplotHost = HostAxes

View File

@ -0,0 +1,394 @@
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 = [*second_limits_, *minute_limits_, *degree_limits_]
degree_steps = [*minsec_steps_, *minsec_steps_, *degree_steps_]
degree_factors = [*second_factors, *minute_factors, *degree_factors]
n = np.searchsorted(degree_limits, 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 = [*second_limits_, *minute_limits_, *hour_limits_]
hour_steps = [*minsec_steps_, *minsec_steps_, *hour_steps_]
hour_factors = [*second_factors, *minute_factors, *hour_factors]
n = np.searchsorted(hour_limits, 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
levs = np.arange(np.floor(v1 * factor / step),
np.ceil(v2 * factor / 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:
def __init__(self, nbins, include_last=True):
self.nbins = nbins
self._include_last = include_last
def set_params(self, nbins=None):
if nbins is not None:
self.nbins = int(nbins)
class LocatorHMS(LocatorBase):
def __call__(self, v1, v2):
return select_step24(v1, v2, self.nbins, self._include_last)
class LocatorHM(LocatorBase):
def __call__(self, v1, v2):
return select_step24(v1, v2, self.nbins, self._include_last,
threshold_factor=60)
class LocatorH(LocatorBase):
def __call__(self, v1, v2):
return select_step24(v1, v2, self.nbins, self._include_last,
threshold_factor=1)
class LocatorDMS(LocatorBase):
def __call__(self, v1, v2):
return select_step360(v1, v2, self.nbins, self._include_last)
class LocatorDM(LocatorBase):
def __call__(self, v1, v2):
return select_step360(v1, v2, self.nbins, self._include_last,
threshold_factor=60)
class LocatorD(LocatorBase):
def __call__(self, v1, v2):
return select_step360(v1, v2, self.nbins, self._include_last,
threshold_factor=1)
class FormatterDMS:
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}$" % 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):
# docstring inherited
def __init__(self, nx, ny,
lon_cycle=360., lat_cycle=None,
lon_minmax=None, lat_minmax=(-90, 90)):
"""
This subclass handles the case where one or both coordinates should be
taken modulo 360, or be restricted to not exceed a specific range.
Parameters
----------
nx, ny : int
The number of samples in each direction.
lon_cycle, lat_cycle : 360 or None
If not None, values in the corresponding direction are taken modulo
*lon_cycle* or *lat_cycle*; in theory this can be any number but
the implementation actually assumes that it is 360 (if not None);
other values give nonsensical results.
This is done by "unwrapping" the transformed grid coordinates so
that jumps are less than a half-cycle; then normalizing the span to
no more than a full cycle.
For example, if values are in the union of the [0, 2] and
[358, 360] intervals (typically, angles measured modulo 360), the
values in the second interval are normalized to [-2, 0] instead so
that the values now cover [-2, 2]. If values are in a range of
[5, 1000], this gets normalized to [5, 365].
lon_minmax, lat_minmax : (float, float) or None
If not None, the computed bounding box is clipped to the given
range in the corresponding direction.
"""
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):
# docstring inherited
x, y = np.meshgrid(
np.linspace(x1, x2, self.nx), np.linspace(y1, y2, self.ny))
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._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,2 @@
from mpl_toolkits.axes_grid1.axes_divider import ( # noqa
Divider, AxesLocator, SubplotDivider, AxesDivider, make_axes_locatable)

View File

@ -0,0 +1,23 @@
from matplotlib import _api
import mpl_toolkits.axes_grid1.axes_grid as axes_grid_orig
from .axislines import Axes
_api.warn_deprecated(
"3.8", name=__name__, obj_type="module", alternative="axes_grid1.axes_grid")
@_api.deprecated("3.8", alternative=(
"axes_grid1.axes_grid.Grid(..., axes_class=axislines.Axes"))
class Grid(axes_grid_orig.Grid):
_defaultAxesClass = Axes
@_api.deprecated("3.8", alternative=(
"axes_grid1.axes_grid.ImageGrid(..., axes_class=axislines.Axes"))
class ImageGrid(axes_grid_orig.ImageGrid):
_defaultAxesClass = Axes
AxesGrid = ImageGrid

View File

@ -0,0 +1,18 @@
from matplotlib import _api
from mpl_toolkits.axes_grid1.axes_rgb import ( # noqa
make_rgb_axes, RGBAxes as _RGBAxes)
from .axislines import Axes
_api.warn_deprecated(
"3.8", name=__name__, obj_type="module", alternative="axes_grid1.axes_rgb")
@_api.deprecated("3.8", alternative=(
"axes_grid1.axes_rgb.RGBAxes(..., axes_class=axislines.Axes"))
class RGBAxes(_RGBAxes):
"""
Subclass of `~.axes_grid1.axes_rgb.RGBAxes` with
``_defaultAxesClass`` = `.axislines.Axes`.
"""
_defaultAxesClass = Axes

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,193 @@
"""
Provides classes to style the axis lines.
"""
import math
import numpy as np
import matplotlib as mpl
from matplotlib.patches import _Style, FancyArrowPatch
from matplotlib.path import Path
from matplotlib.transforms import IdentityTransform
class _FancyAxislineStyle:
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,
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.
"""
(x0, y0), (x1, y1) = path.vertices[-2:]
theta = math.atan2(y1 - y0, x1 - x0)
x2 = x1 + math.cos(theta) * mutation_size
y2 = y1 + math.sin(theta) * mutation_size
if path.codes is None:
return Path(np.concatenate([path.vertices, [[x2, y2]]]))
else:
return Path(np.concatenate([path.vertices, [[x2, y2]]]),
np.concatenate([path.codes, [Path.LINETO]]))
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)
def get_window_extent(self, renderer=None):
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
return FancyArrowPatch.get_window_extent(self, renderer)
class FilledArrow(SimpleArrow):
"""The artist class that will be returned for FilledArrow style."""
_ARROW_STYLE = "-|>"
def __init__(self, axis_artist, line_path, transform,
line_mutation_scale, facecolor):
super().__init__(axis_artist, line_path, transform,
line_mutation_scale)
self.set_facecolor(facecolor)
class AxislineStyle(_Style):
"""
A container class which defines style classes for AxisArtists.
An instance of any axisline style class is a callable object,
whose call signature is ::
__call__(self, axis_artist, path, transform)
When called, this should return an `.Artist` with the following methods::
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:
# 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 Matplotlib 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):
"""
Parameters
----------
size : float
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):
"""
An arrow with a filled head.
"""
ArrowAxisClass = _FancyAxislineStyle.FilledArrow
def __init__(self, size=1, facecolor=None):
"""
Parameters
----------
size : float
Size of the arrow as a fraction of the ticklabel size.
facecolor : :mpltype:`color`, default: :rc:`axes.edgecolor`
Fill color.
.. versionadded:: 3.7
"""
if facecolor is None:
facecolor = mpl.rcParams['axes.edgecolor']
self.size = size
self._facecolor = facecolor
super().__init__(size=size)
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,
facecolor=self._facecolor)
return axisline
_style_list["-|>"] = FilledArrow

View File

@ -0,0 +1,483 @@
"""
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 Matplotlib'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 Matplotlib, 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 the 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 `matplotlib.axis.Axis`, thus most
tick-related functions in Matplotlib won't work. 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
import matplotlib as mpl
from matplotlib import _api
import matplotlib.axes as maxes
from matplotlib.path import Path
from mpl_toolkits.axes_grid1 import mpl_axes
from .axisline_style import AxislineStyle # noqa
from .axis_artist import AxisArtist, GridlinesCollection
class _AxisArtistHelperBase:
"""
Base class for axis helper.
Subclasses should define the methods listed below. The *axes*
argument will be the ``.axes`` attribute of the caller artist. ::
# Construct the spine.
def get_line_transform(self, axes):
return transform
def get_line(self, axes):
return path
# Construct the label.
def get_axislabel_transform(self, axes):
return transform
def get_axislabel_pos_angle(self, axes):
return (x, y), angle
# Construct the ticks.
def get_tick_transform(self, axes):
return transform
def get_tick_iterators(self, axes):
# A pair of iterables (one for major ticks, one for minor ticks)
# that yield (tick_position, tick_angle, tick_label).
return iter_major, iter_minor
"""
def __init__(self, nth_coord):
self.nth_coord = nth_coord
def update_lim(self, axes):
pass
def get_nth_coord(self):
return self.nth_coord
def _to_xy(self, values, const):
"""
Create a (*values.shape, 2)-shape array representing (x, y) pairs.
The other coordinate is filled with the constant *const*.
Example::
>>> self.nth_coord = 0
>>> self._to_xy([1, 2, 3], const=0)
array([[1, 0],
[2, 0],
[3, 0]])
"""
if self.nth_coord == 0:
return np.stack(np.broadcast_arrays(values, const), axis=-1)
elif self.nth_coord == 1:
return np.stack(np.broadcast_arrays(const, values), axis=-1)
else:
raise ValueError("Unexpected nth_coord")
class _FixedAxisArtistHelperBase(_AxisArtistHelperBase):
"""Helper class for a fixed (in the axes coordinate) axis."""
@_api.delete_parameter("3.9", "nth_coord")
def __init__(self, loc, nth_coord=None):
"""``nth_coord = 0``: x-axis; ``nth_coord = 1``: y-axis."""
super().__init__(_api.check_getitem(
{"bottom": 0, "top": 0, "left": 1, "right": 1}, loc=loc))
self._loc = loc
self._pos = {"bottom": 0, "top": 1, "left": 0, "right": 1}[loc]
# axis line in transAxes
self._path = Path(self._to_xy((0, 1), const=self._pos))
# 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):
"""
Return the label reference position in transAxes.
get_label_transform() returns a transform of (transAxes+offset)
"""
return dict(left=((0., 0.5), 90), # (position, angle_tangent)
right=((1., 0.5), 90),
bottom=((0.5, 0.), 0),
top=((0.5, 1.), 0))[self._loc]
# TICK
def get_tick_transform(self, axes):
return [axes.get_xaxis_transform(), axes.get_yaxis_transform()][self.nth_coord]
class _FloatingAxisArtistHelperBase(_AxisArtistHelperBase):
def __init__(self, nth_coord, value):
self._value = value
super().__init__(nth_coord)
def get_line(self, axes):
raise RuntimeError("get_line method should be defined by the derived class")
class FixedAxisArtistHelperRectilinear(_FixedAxisArtistHelperBase):
@_api.delete_parameter("3.9", "nth_coord")
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)
self.axis = [axes.xaxis, axes.yaxis][self.nth_coord]
# TICK
def get_tick_iterators(self, axes):
"""tick_loc, tick_angle, tick_label"""
angle_normal, angle_tangent = {0: (90, 0), 1: (0, 90)}[self.nth_coord]
major = self.axis.major
major_locs = major.locator()
major_labels = major.formatter.format_ticks(major_locs)
minor = self.axis.minor
minor_locs = minor.locator()
minor_labels = minor.formatter.format_ticks(minor_locs)
tick_to_axes = self.get_tick_transform(axes) - axes.transAxes
def _f(locs, labels):
for loc, label in zip(locs, labels):
c = self._to_xy(loc, const=self._pos)
# check if the tick point is inside axes
c2 = tick_to_axes.transform(c)
if mpl.transforms._interval_contains_close((0, 1), c2[self.nth_coord]):
yield c, angle_normal, angle_tangent, label
return _f(major_locs, major_labels), _f(minor_locs, minor_labels)
class FloatingAxisArtistHelperRectilinear(_FloatingAxisArtistHelperBase):
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):
fixed_coord = 1 - self.nth_coord
data_to_axes = axes.transData - axes.transAxes
p = data_to_axes.transform([self._value, self._value])
return Path(self._to_xy((0, 1), const=p[fixed_coord]))
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):
"""
Return the label reference position in transAxes.
get_label_transform() returns a transform of (transAxes+offset)
"""
angle = [0, 90][self.nth_coord]
fixed_coord = 1 - self.nth_coord
data_to_axes = axes.transData - axes.transAxes
p = data_to_axes.transform([self._value, self._value])
verts = self._to_xy(0.5, const=p[fixed_coord])
return (verts, angle) if 0 <= verts[fixed_coord] <= 1 else (None, None)
def get_tick_transform(self, axes):
return axes.transData
def get_tick_iterators(self, axes):
"""tick_loc, tick_angle, tick_label"""
angle_normal, angle_tangent = {0: (90, 0), 1: (0, 90)}[self.nth_coord]
major = self.axis.major
major_locs = major.locator()
major_labels = major.formatter.format_ticks(major_locs)
minor = self.axis.minor
minor_locs = minor.locator()
minor_labels = minor.formatter.format_ticks(minor_locs)
data_to_axes = axes.transData - axes.transAxes
def _f(locs, labels):
for loc, label in zip(locs, labels):
c = self._to_xy(loc, const=self._value)
c1, c2 = data_to_axes.transform(c)
if 0 <= c1 <= 1 and 0 <= c2 <= 1:
yield c, angle_normal, angle_tangent, label
return _f(major_locs, major_labels), _f(minor_locs, minor_labels)
class AxisArtistHelper: # Backcompat.
Fixed = _FixedAxisArtistHelperBase
Floating = _FloatingAxisArtistHelperBase
class AxisArtistHelperRectlinear: # Backcompat.
Fixed = FixedAxisArtistHelperRectilinear
Floating = FloatingAxisArtistHelperRectilinear
class GridHelperBase:
def __init__(self):
self._old_limits = None
super().__init__()
def update_lim(self, axes):
x1, x2 = axes.get_xlim()
y1, y2 = axes.get_ylim()
if self._old_limits != (x1, x2, y1, y2):
self._update_grid(x1, y1, x2, y2)
self._old_limits = (x1, x2, y1, y2)
def _update_grid(self, x1, y1, x2, y2):
"""Cache relevant computations when the axes limits have changed."""
def get_gridlines(self, which, axis):
"""
Return list of grid lines as a list of paths (list of points).
Parameters
----------
which : {"both", "major", "minor"}
axis : {"both", "x", "y"}
"""
return []
class GridHelperRectlinear(GridHelperBase):
def __init__(self, axes):
super().__init__()
self.axes = axes
@_api.delete_parameter(
"3.9", "nth_coord", addendum="'nth_coord' is now inferred from 'loc'.")
def new_fixed_axis(
self, loc, nth_coord=None, axis_direction=None, offset=None, axes=None):
if axes is None:
_api.warn_external(
"'new_fixed_axis' explicitly requires the axes keyword.")
axes = self.axes
if axis_direction is None:
axis_direction = loc
return AxisArtist(axes, FixedAxisArtistHelperRectilinear(axes, loc),
offset=offset, axis_direction=axis_direction)
def new_floating_axis(self, nth_coord, value, axis_direction="bottom", axes=None):
if axes is None:
_api.warn_external(
"'new_floating_axis' explicitly requires the axes keyword.")
axes = self.axes
helper = FloatingAxisArtistHelperRectilinear(
axes, nth_coord, value, axis_direction)
axisline = AxisArtist(axes, helper, axis_direction=axis_direction)
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.
Parameters
----------
which : {"both", "major", "minor"}
axis : {"both", "x", "y"}
"""
_api.check_in_list(["both", "major", "minor"], which=which)
_api.check_in_list(["both", "x", "y"], axis=axis)
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())
gridlines.extend([[x, x], [y1, y2]] for x in locs)
if axis in ("both", "y"):
x1, x2 = self.axes.get_xlim()
locs = []
if self.axes.yaxis._major_tick_kw["gridOn"]:
locs.extend(self.axes.yaxis.major.locator())
if self.axes.yaxis._minor_tick_kw["gridOn"]:
locs.extend(self.axes.yaxis.minor.locator())
gridlines.extend([[x1, x2], [y, y]] for y in locs)
return gridlines
class Axes(maxes.Axes):
@_api.deprecated("3.8", alternative="ax.axis")
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
self.spines[:].set_visible(False)
self.xaxis.set_visible(False)
self.yaxis.set_visible(False)
else:
self._axisline_on = False
self.spines[:].set_visible(True)
self.xaxis.set_visible(True)
self.yaxis.set_visible(True)
@property
def axis(self):
return self._axislines
def clear(self):
# docstring inherited
# Init gridlines before clear() as clear() calls grid().
self.gridlines = gridlines = GridlinesCollection(
[],
colors=mpl.rcParams['grid.color'],
linestyles=mpl.rcParams['grid.linestyle'],
linewidths=mpl.rcParams['grid.linewidth'])
self._set_artist_props(gridlines)
gridlines.set_grid_helper(self.get_grid_helper())
super().clear()
# clip_path is set after Axes.clear(): that's when a patch is created.
gridlines.set_clip_path(self.axes.patch)
# Init axis artists.
self._axislines = mpl_axes.Axes.AxisDict(self)
new_fixed_axis = self.get_grid_helper().new_fixed_axis
self._axislines.update({
loc: new_fixed_axis(loc=loc, axes=self, axis_direction=loc)
for loc in ["bottom", "top", "left", "right"]})
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)
def get_grid_helper(self):
return self._grid_helper
def grid(self, visible=None, which='major', axis="both", **kwargs):
"""
Toggle the gridlines, and optionally set the properties of the lines.
"""
# There are some discrepancies in the behavior of grid() between
# axes_grid and Matplotlib, because axes_grid explicitly sets the
# visibility of the gridlines.
super().grid(visible, which=which, axis=axis, **kwargs)
if not self._axisline_on:
return
if visible is None:
visible = (self.axes.xaxis._minor_tick_kw["gridOn"]
or self.axes.xaxis._major_tick_kw["gridOn"]
or self.axes.yaxis._minor_tick_kw["gridOn"]
or self.axes.yaxis._major_tick_kw["gridOn"])
self.gridlines.set(which=which, axis=axis, visible=visible)
self.gridlines.set(**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 new_fixed_axis(self, loc, offset=None):
return self.get_grid_helper().new_fixed_axis(loc, offset=offset, axes=self)
def new_floating_axis(self, nth_coord, value, axis_direction="bottom"):
return self.get_grid_helper().new_floating_axis(
nth_coord, value, axis_direction=axis_direction, axes=self)
class AxesZero(Axes):
def clear(self):
super().clear()
new_floating_axis = self.get_grid_helper().new_floating_axis
self._axislines.update(
xzero=new_floating_axis(
nth_coord=0, value=0., axis_direction="bottom", axes=self),
yzero=new_floating_axis(
nth_coord=1, value=0., axis_direction="left", axes=self),
)
for k in ["xzero", "yzero"]:
self._axislines[k].line.set_clip_path(self.patch)
self._axislines[k].set_visible(False)
Subplot = Axes
SubplotZero = AxesZero

View File

@ -0,0 +1,286 @@
"""
An experimental support for curvilinear grid.
"""
# TODO :
# see if tick_iterator method can be simplified by reusing the parent method.
import functools
import numpy as np
import matplotlib as mpl
from matplotlib import _api, cbook
import matplotlib.patches as mpatches
from matplotlib.path import Path
from mpl_toolkits.axes_grid1.parasite_axes import host_axes_class_factory
from . import axislines, grid_helper_curvelinear
from .axis_artist import AxisArtist
from .grid_finder import ExtremeFinderSimple
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
"""
lon1, lon2, lat1, lat2 = grid_helper.grid_finder.extreme_finder(*[None] * 5)
value, nth_coord = _api.check_getitem(
dict(left=(lon1, 0), right=(lon2, 0), bottom=(lat1, 1), top=(lat2, 1)),
side=side)
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_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"]
yy0 = lat_levs / lat_factor
lon_levs, lon_n, lon_factor = self._grid_info["lon_info"]
xx0 = lon_levs / lon_factor
extremes = self.grid_helper.grid_finder.extreme_finder(*[None] * 5)
xmin, xmax = sorted(extremes[:2])
ymin, ymax = sorted(extremes[2:])
def trf_xy(x, y):
trf = grid_finder.get_transform() + axes.transData
return trf.transform(np.column_stack(np.broadcast_arrays(x, y))).T
if self.nth_coord == 0:
mask = (ymin <= yy0) & (yy0 <= ymax)
(xx1, yy1), (dxx1, dyy1), (dxx2, dyy2) = \
grid_helper_curvelinear._value_and_jacobian(
trf_xy, self.value, yy0[mask], (xmin, xmax), (ymin, ymax))
labels = self._grid_info["lat_labels"]
elif self.nth_coord == 1:
mask = (xmin <= xx0) & (xx0 <= xmax)
(xx1, yy1), (dxx2, dyy2), (dxx1, dyy1) = \
grid_helper_curvelinear._value_and_jacobian(
trf_xy, xx0[mask], self.value, (xmin, xmax), (ymin, ymax))
labels = self._grid_info["lon_labels"]
labels = [l for l, m in zip(labels, mask) if m]
angle_normal = np.arctan2(dyy1, dxx1)
angle_tangent = np.arctan2(dyy2, dxx2)
mm = (dyy1 == 0) & (dxx1 == 0) # points with degenerate normal
angle_normal[mm] = angle_tangent[mm] + np.pi / 2
tick_to_axes = self.get_tick_transform(axes) - axes.transAxes
in_01 = functools.partial(
mpl.transforms._interval_contains_close, (0, 1))
def f1():
for x, y, normal, tangent, lab \
in zip(xx1, yy1, angle_normal, angle_tangent, labels):
c2 = tick_to_axes.transform((x, y))
if in_01(c2[0]) and in_01(c2[1]):
yield [x, y], *np.rad2deg([normal, tangent]), lab
return f1(), iter([])
def get_line(self, axes):
self.update_lim(axes)
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]))
class ExtremeFinderFixed(ExtremeFinderSimple):
# docstring inherited
def __init__(self, extremes):
"""
This subclass always returns the same bounding box.
Parameters
----------
extremes : (float, float, float, float)
The bounding box that this helper always returns.
"""
self._extremes = extremes
def __call__(self, transform_xy, x1, y1, x2, y2):
# docstring inherited
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):
# docstring inherited
super().__init__(aux_trans,
extreme_finder=ExtremeFinderFixed(extremes),
grid_locator1=grid_locator1,
grid_locator2=grid_locator2,
tick_formatter1=tick_formatter1,
tick_formatter2=tick_formatter2)
@_api.deprecated("3.8")
def get_data_boundary(self, side):
"""
Return v=0, nth=1.
"""
lon1, lon2, lat1, lat2 = self.grid_finder.extreme_finder(*[None] * 5)
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
# This is not the same as the FixedAxisArtistHelper class used by
# grid_helper_curvelinear.GridHelperCurveLinear.new_fixed_axis!
helper = FixedAxisArtistHelper(
self, loc, nth_coord_ticks=nth_coord)
axisline = AxisArtist(axes, helper, axis_direction=axis_direction)
# Perhaps should be moved to the base class?
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):
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:])
grid_info["extremes"] = lon_min, lon_max, lat_min, lat_max # extremes
lon_levs, lon_n, lon_factor = \
grid_finder.grid_locator1(lon_min, lon_max)
lon_levs = np.asarray(lon_levs)
lat_levs, lat_n, lat_factor = \
grid_finder.grid_locator2(lat_min, lat_max)
lat_levs = np.asarray(lat_levs)
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._format_ticks(
1, "bottom", lon_factor, lon_levs)
grid_info["lat_labels"] = grid_finder._format_ticks(
2, "bottom", lat_factor, lat_levs)
lon_values = lon_levs[:lon_n] / lon_factor
lat_values = lat_levs[:lat_n] / lat_factor
lon_lines, lat_lines = grid_finder._get_raw_grid_lines(
lon_values[(lon_min < lon_values) & (lon_values < lon_max)],
lat_values[(lat_min < lat_values) & (lat_values < lat_max)],
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(
# lon_min, lon_max, lat_min, lat_max)
extremes[:2], extremes[2:], *extremes)
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"]:
grid_lines.extend(self._grid_info["lon_lines"])
if axis in ["both", "y"]:
grid_lines.extend(self._grid_info["lat_lines"])
return grid_lines
class FloatingAxesBase:
def __init__(self, *args, grid_helper, **kwargs):
_api.check_isinstance(GridHelperCurveLinear, grid_helper=grid_helper)
super().__init__(*args, grid_helper=grid_helper, **kwargs)
self.set_aspect(1.)
def _gen_axes_patch(self):
# docstring inherited
x0, x1, y0, y1 = self.get_grid_helper().grid_finder.extreme_finder(*[None] * 5)
patch = mpatches.Polygon([(x0, y0), (x1, y0), (x1, y1), (x0, y1)])
patch.get_path()._interpolation_steps = 100
return patch
def clear(self):
super().clear()
self.patch.set_transform(
self.get_grid_helper().grid_finder.get_transform()
+ self.transData)
# The original patch is not in the draw tree; it is only used for
# clipping purposes.
orig_patch = super()._gen_axes_patch()
orig_patch.set_figure(self.figure)
orig_patch.set_transform(self.transAxes)
self.patch.set_clip_path(orig_patch)
self.gridlines.set_clip_path(orig_patch)
self.adjust_axes_lim()
def adjust_axes_lim(self):
bbox = self.patch.get_path().get_extents(
# First transform to pixel coords, then to parent data coords.
self.patch.get_transform() - self.transData)
bbox = bbox.expanded(1.02, 1.02)
self.set_xlim(bbox.xmin, bbox.xmax)
self.set_ylim(bbox.ymin, bbox.ymax)
floatingaxes_class_factory = cbook._make_class_factory(FloatingAxesBase, "Floating{}")
FloatingAxes = floatingaxes_class_factory(host_axes_class_factory(axislines.Axes))
FloatingSubplot = FloatingAxes

View File

@ -0,0 +1,326 @@
import numpy as np
from matplotlib import ticker as mticker, _api
from matplotlib.transforms import Bbox, Transform
def _find_line_box_crossings(xys, bbox):
"""
Find the points where a polyline crosses a bbox, and the crossing angles.
Parameters
----------
xys : (N, 2) array
The polyline coordinates.
bbox : `.Bbox`
The bounding box.
Returns
-------
list of ((float, float), float)
Four separate lists of crossings, for the left, right, bottom, and top
sides of the bbox, respectively. For each list, the entries are the
``((x, y), ccw_angle_in_degrees)`` of the crossing, where an angle of 0
means that the polyline is moving to the right at the crossing point.
The entries are computed by linearly interpolating at each crossing
between the nearest points on either side of the bbox edges.
"""
crossings = []
dxys = xys[1:] - xys[:-1]
for sl in [slice(None), slice(None, None, -1)]:
us, vs = xys.T[sl] # "this" coord, "other" coord
dus, dvs = dxys.T[sl]
umin, vmin = bbox.min[sl]
umax, vmax = bbox.max[sl]
for u0, inside in [(umin, us > umin), (umax, us < umax)]:
cross = []
idxs, = (inside[:-1] ^ inside[1:]).nonzero()
for idx in idxs:
v = vs[idx] + (u0 - us[idx]) * dvs[idx] / dus[idx]
if not vmin <= v <= vmax:
continue
crossing = (u0, v)[sl]
theta = np.degrees(np.arctan2(*dxys[idx][::-1]))
cross.append((crossing, theta))
crossings.append(cross)
return crossings
class ExtremeFinderSimple:
"""
A helper class to figure out the range of grid lines that need to be drawn.
"""
def __init__(self, nx, ny):
"""
Parameters
----------
nx, ny : int
The number of samples in each direction.
"""
self.nx = nx
self.ny = ny
def __call__(self, transform_xy, x1, y1, x2, y2):
"""
Compute an approximation of the bounding box obtained by applying
*transform_xy* to the box delimited by ``(x1, y1, x2, y2)``.
The intended use is to have ``(x1, y1, x2, y2)`` in axes coordinates,
and have *transform_xy* be the transform from axes coordinates to data
coordinates; this method then returns the range of data coordinates
that span the actual axes.
The computation is done by sampling ``nx * ny`` equispaced points in
the ``(x1, y1, x2, y2)`` box and finding the resulting points with
extremal coordinates; then adding some padding to take into account the
finite sampling.
As each sampling step covers a relative range of *1/nx* or *1/ny*,
the padding is computed by expanding the span covered by the extremal
coordinates by these fractions.
"""
x, y = np.meshgrid(
np.linspace(x1, x2, self.nx), np.linspace(y1, y2, self.ny))
xt, yt = transform_xy(np.ravel(x), np.ravel(y))
return self._add_pad(xt.min(), xt.max(), yt.min(), yt.max())
def _add_pad(self, x_min, x_max, y_min, y_max):
"""Perform the padding mentioned in `__call__`."""
dx = (x_max - x_min) / self.nx
dy = (y_max - y_min) / self.ny
return x_min - dx, x_max + dx, y_min - dy, y_max + dy
class _User2DTransform(Transform):
"""A transform defined by two user-set functions."""
input_dims = output_dims = 2
def __init__(self, forward, backward):
"""
Parameters
----------
forward, backward : callable
The forward and backward transforms, taking ``x`` and ``y`` as
separate arguments and returning ``(tr_x, tr_y)``.
"""
# The normal Matplotlib convention would be to take and return an
# (N, 2) array but axisartist uses the transposed version.
super().__init__()
self._forward = forward
self._backward = backward
def transform_non_affine(self, values):
# docstring inherited
return np.transpose(self._forward(*np.transpose(values)))
def inverted(self):
# docstring inherited
return type(self)(self._backward, self._forward)
class GridFinder:
"""
Internal helper for `~.grid_helper_curvelinear.GridHelperCurveLinear`, with
the same constructor parameters; should not be directly instantiated.
"""
def __init__(self,
transform,
extreme_finder=None,
grid_locator1=None,
grid_locator2=None,
tick_formatter1=None,
tick_formatter2=None):
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()
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
self.set_transform(transform)
def _format_ticks(self, idx, direction, factor, levels):
"""
Helper to support both standard formatters (inheriting from
`.mticker.Formatter`) and axisartist-specific ones; should be called instead of
directly calling ``self.tick_formatter1`` and ``self.tick_formatter2``. This
method should be considered as a temporary workaround which will be removed in
the future at the same time as axisartist-specific formatters.
"""
fmt = _api.check_getitem(
{1: self.tick_formatter1, 2: self.tick_formatter2}, idx=idx)
return (fmt.format_ticks(levels) if isinstance(fmt, mticker.Formatter)
else fmt(direction, factor, levels))
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)
lon_levs = np.asarray(lon_levs)
lat_levs, lat_n, lat_factor = self.grid_locator2(lat_min, lat_max)
lat_levs = np.asarray(lat_levs)
lon_values = lon_levs[:lon_n] / lon_factor
lat_values = 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)
bb = Bbox.from_extents(x1, y1, x2, y2).expanded(1 + 2e-10, 1 + 2e-10)
grid_info = {
"extremes": extremes,
# "lon", "lat", filled below.
}
for idx, lon_or_lat, levs, factor, values, lines in [
(1, "lon", lon_levs, lon_factor, lon_values, lon_lines),
(2, "lat", lat_levs, lat_factor, lat_values, lat_lines),
]:
grid_info[lon_or_lat] = gi = {
"lines": [[l] for l in lines],
"ticks": {"left": [], "right": [], "bottom": [], "top": []},
}
for (lx, ly), v, level in zip(lines, values, levs):
all_crossings = _find_line_box_crossings(np.column_stack([lx, ly]), bb)
for side, crossings in zip(
["left", "right", "bottom", "top"], all_crossings):
for crossing in crossings:
gi["ticks"][side].append({"level": level, "loc": crossing})
for side in gi["ticks"]:
levs = [tick["level"] for tick in gi["ticks"][side]]
labels = self._format_ticks(idx, side, factor, levs)
for tick, label in zip(gi["ticks"][side], labels):
tick["label"] = label
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.full_like(lats_i, lon), lats_i)
for lon in lon_values]
lat_lines = [self.transform_xy(lons_i, np.full_like(lons_i, lat))
for lat in lat_values]
return lon_lines, lat_lines
def set_transform(self, aux_trans):
if isinstance(aux_trans, Transform):
self._aux_transform = aux_trans
elif len(aux_trans) == 2 and all(map(callable, aux_trans)):
self._aux_transform = _User2DTransform(*aux_trans)
else:
raise TypeError("'aux_trans' must be either a Transform "
"instance or a pair of callables")
def get_transform(self):
return self._aux_transform
update_transform = set_transform # backcompat alias.
def transform_xy(self, x, y):
return self._aux_transform.transform(np.column_stack([x, y])).T
def inv_transform_xy(self, x, y):
return self._aux_transform.inverted().transform(
np.column_stack([x, y])).T
def update(self, **kwargs):
for k, v in kwargs.items():
if k in ["extreme_finder",
"grid_locator1",
"grid_locator2",
"tick_formatter1",
"tick_formatter2"]:
setattr(self, k, v)
else:
raise ValueError(f"Unknown update property {k!r}")
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
super().__init__(nbins, steps=steps, integer=integer,
symmetric=symmetric, prune=prune)
self.create_dummy_axis()
def __call__(self, v1, v2):
locs = super().tick_values(v1, v2)
return np.array(locs), len(locs), 1 # 1: factor (see angle_helper)
class FixedLocator:
def __init__(self, locs):
self._locs = locs
def __call__(self, v1, v2):
v1, v2 = sorted([v1, v2])
locs = np.array([l for l in self._locs if v1 <= l <= v2])
return locs, len(locs), 1 # 1: factor (see angle_helper)
# Tick Formatter
class FormatterPrettyPrint:
def __init__(self, useMathText=True):
self._fmt = mticker.ScalarFormatter(
useMathText=useMathText, useOffset=False)
self._fmt.create_dummy_axis()
def __call__(self, direction, factor, values):
return self._fmt.format_ticks(values)
class DictFormatter:
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)
return [self._format_dict.get(k, v)
for k, v in zip(values, fallback_strings)]

View File

@ -0,0 +1,328 @@
"""
An experimental support for curvilinear grid.
"""
import functools
import numpy as np
import matplotlib as mpl
from matplotlib import _api
from matplotlib.path import Path
from matplotlib.transforms import Affine2D, IdentityTransform
from .axislines import (
_FixedAxisArtistHelperBase, _FloatingAxisArtistHelperBase, GridHelperBase)
from .axis_artist import AxisArtist
from .grid_finder import GridFinder
def _value_and_jacobian(func, xs, ys, xlims, ylims):
"""
Compute *func* and its derivatives along x and y at positions *xs*, *ys*,
while ensuring that finite difference calculations don't try to evaluate
values outside of *xlims*, *ylims*.
"""
eps = np.finfo(float).eps ** (1/2) # see e.g. scipy.optimize.approx_fprime
val = func(xs, ys)
# Take the finite difference step in the direction where the bound is the
# furthest; the step size is min of epsilon and distance to that bound.
xlo, xhi = sorted(xlims)
dxlo = xs - xlo
dxhi = xhi - xs
xeps = (np.take([-1, 1], dxhi >= dxlo)
* np.minimum(eps, np.maximum(dxlo, dxhi)))
val_dx = func(xs + xeps, ys)
ylo, yhi = sorted(ylims)
dylo = ys - ylo
dyhi = yhi - ys
yeps = (np.take([-1, 1], dyhi >= dylo)
* np.minimum(eps, np.maximum(dylo, dyhi)))
val_dy = func(xs, ys + yeps)
return (val, (val_dx - val) / xeps, (val_dy - val) / yeps)
class FixedAxisArtistHelper(_FixedAxisArtistHelperBase):
"""
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
def update_lim(self, axes):
self.grid_helper.update_lim(axes)
def get_tick_transform(self, axes):
return axes.transData
def get_tick_iterators(self, axes):
"""tick_loc, tick_angle, tick_label"""
v1, v2 = axes.get_ylim() if self.nth_coord == 0 else axes.get_xlim()
if v1 > v2: # Inverted limits.
side = {"left": "right", "right": "left",
"top": "bottom", "bottom": "top"}[self.side]
else:
side = self.side
angle_tangent = dict(left=90, right=90, bottom=0, top=0)[side]
def iter_major():
for nth_coord, show_labels in [
(self.nth_coord_ticks, True), (1 - self.nth_coord_ticks, False)]:
gi = self.grid_helper._grid_info[["lon", "lat"][nth_coord]]
for tick in gi["ticks"][side]:
yield (*tick["loc"], angle_tangent,
(tick["label"] if show_labels else ""))
return iter_major(), iter([])
class FloatingAxisArtistHelper(_FloatingAxisArtistHelperBase):
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 = -np.inf, np.inf
self._line_num_points = 100 # number of points to create a line
def set_extremes(self, e1, e2):
if e1 is None:
e1 = -np.inf
if e2 is None:
e2 = np.inf
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)
lon_min, lon_max, lat_min, lat_max = extremes
e_min, e_max = self._extremes # ranges of other coordinates
if self.nth_coord == 0:
lat_min = max(e_min, lat_min)
lat_max = min(e_max, lat_max)
elif self.nth_coord == 1:
lon_min = max(e_min, lon_min)
lon_max = min(e_max, lon_max)
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)
if self.nth_coord == 0:
xx0 = np.full(self._line_num_points, self.value)
yy0 = np.linspace(lat_min, lat_max, self._line_num_points)
xx, yy = grid_finder.transform_xy(xx0, yy0)
elif self.nth_coord == 1:
xx0 = np.linspace(lon_min, lon_max, self._line_num_points)
yy0 = np.full(self._line_num_points, self.value)
xx, yy = grid_finder.transform_xy(xx0, yy0)
self._grid_info = {
"extremes": (lon_min, lon_max, lat_min, lat_max),
"lon_info": (lon_levs, lon_n, np.asarray(lon_factor)),
"lat_info": (lat_levs, lat_n, np.asarray(lat_factor)),
"lon_labels": grid_finder._format_ticks(
1, "bottom", lon_factor, lon_levs),
"lat_labels": grid_finder._format_ticks(
2, "bottom", lat_factor, lat_levs),
"line_xy": (xx, yy),
}
def get_axislabel_transform(self, axes):
return Affine2D() # axes.transData
def get_axislabel_pos_angle(self, axes):
def trf_xy(x, y):
trf = self.grid_helper.grid_finder.get_transform() + axes.transData
return trf.transform([x, y]).T
xmin, xmax, ymin, ymax = self._grid_info["extremes"]
if self.nth_coord == 0:
xx0 = self.value
yy0 = (ymin + ymax) / 2
elif self.nth_coord == 1:
xx0 = (xmin + xmax) / 2
yy0 = self.value
xy1, dxy1_dx, dxy1_dy = _value_and_jacobian(
trf_xy, xx0, yy0, (xmin, xmax), (ymin, ymax))
p = axes.transAxes.inverted().transform(xy1)
if 0 <= p[0] <= 1 and 0 <= p[1] <= 1:
d = [dxy1_dy, dxy1_dx][self.nth_coord]
return xy1, np.rad2deg(np.arctan2(*d[::-1]))
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"""
lat_levs, lat_n, lat_factor = self._grid_info["lat_info"]
yy0 = lat_levs / lat_factor
lon_levs, lon_n, lon_factor = self._grid_info["lon_info"]
xx0 = lon_levs / lon_factor
e0, e1 = self._extremes
def trf_xy(x, y):
trf = self.grid_helper.grid_finder.get_transform() + axes.transData
return trf.transform(np.column_stack(np.broadcast_arrays(x, y))).T
# find angles
if self.nth_coord == 0:
mask = (e0 <= yy0) & (yy0 <= e1)
(xx1, yy1), (dxx1, dyy1), (dxx2, dyy2) = _value_and_jacobian(
trf_xy, self.value, yy0[mask], (-np.inf, np.inf), (e0, e1))
labels = self._grid_info["lat_labels"]
elif self.nth_coord == 1:
mask = (e0 <= xx0) & (xx0 <= e1)
(xx1, yy1), (dxx2, dyy2), (dxx1, dyy1) = _value_and_jacobian(
trf_xy, xx0[mask], self.value, (-np.inf, np.inf), (e0, e1))
labels = self._grid_info["lon_labels"]
labels = [l for l, m in zip(labels, mask) if m]
angle_normal = np.arctan2(dyy1, dxx1)
angle_tangent = np.arctan2(dyy2, dxx2)
mm = (dyy1 == 0) & (dxx1 == 0) # points with degenerate normal
angle_normal[mm] = angle_tangent[mm] + np.pi / 2
tick_to_axes = self.get_tick_transform(axes) - axes.transAxes
in_01 = functools.partial(
mpl.transforms._interval_contains_close, (0, 1))
def iter_major():
for x, y, normal, tangent, lab \
in zip(xx1, yy1, angle_normal, angle_tangent, labels):
c2 = tick_to_axes.transform((x, y))
if in_01(c2[0]) and in_01(c2[1]):
yield [x, y], *np.rad2deg([normal, tangent]), lab
return iter_major(), 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"]
return Path(np.column_stack([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):
"""
Parameters
----------
aux_trans : `.Transform` or tuple[Callable, Callable]
The transform from curved coordinates to rectilinear coordinate:
either a `.Transform` instance (which provides also its inverse),
or a pair of callables ``(trans, inv_trans)`` that define the
transform and its inverse. The callables should have signature::
x_rect, y_rect = trans(x_curved, y_curved)
x_curved, y_curved = inv_trans(x_rect, y_rect)
extreme_finder
grid_locator1, grid_locator2
Grid locators for each axis.
tick_formatter1, tick_formatter2
Tick formatters for each axis.
"""
super().__init__()
self._grid_info = None
self.grid_finder = GridFinder(aux_trans,
extreme_finder,
grid_locator1,
grid_locator2,
tick_formatter1,
tick_formatter2)
def update_grid_finder(self, aux_trans=None, **kwargs):
if aux_trans is not None:
self.grid_finder.update_transform(aux_trans)
self.grid_finder.update(**kwargs)
self._old_limits = None # Force revalidation.
@_api.make_keyword_only("3.9", "nth_coord")
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)
# Why is clip not set on axisline, unlike in new_floating_axis or in
# the floating_axig.GridHelperCurveLinear subclass?
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)
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)
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
@_api.deprecated("3.9")
def get_tick_iterator(self, nth_coord, axis_side, minor=False):
angle_tangent = dict(left=90, right=90, bottom=0, top=0)[axis_side]
lon_or_lat = ["lon", "lat"][nth_coord]
if not minor: # major ticks
for tick in self._grid_info[lon_or_lat]["ticks"][axis_side]:
yield *tick["loc"], angle_tangent, tick["label"]
else:
for tick in self._grid_info[lon_or_lat]["ticks"][axis_side]:
yield *tick["loc"], angle_tangent, ""

View File

@ -0,0 +1,7 @@
from mpl_toolkits.axes_grid1.parasite_axes import (
host_axes_class_factory, parasite_axes_class_factory)
from .axislines import Axes
ParasiteAxes = parasite_axes_class_factory(Axes)
HostAxes = SubplotHost = host_axes_class_factory(Axes)

View File

@ -0,0 +1,10 @@
from pathlib import Path
# Check that the test directories exist
if not (Path(__file__).parent / "baseline_images").exists():
raise OSError(
'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.')

View File

@ -0,0 +1,2 @@
from matplotlib.testing.conftest import (mpl_test_settings, # noqa
pytest_configure, pytest_unconfigure)

View File

@ -0,0 +1,141 @@
import re
import numpy as np
import pytest
from mpl_toolkits.axisartist.angle_helper import (
FormatterDMS, FormatterHMS, select_step, select_step24, select_step360)
_MS_RE = (
r'''\$ # Mathtext
(
# The sign sometimes appears on a 0 when a fraction is shown.
# Check later that there's only one.
(?P<degree_sign>-)?
(?P<degree>[0-9.]+) # Degrees value
{degree} # Degree symbol (to be replaced by format.)
)?
(
(?(degree)\\,) # Separator if degrees are also visible.
(?P<minute_sign>-)?
(?P<minute>[0-9.]+) # Minutes value
{minute} # Minute symbol (to be replaced by format.)
)?
(
(?(minute)\\,) # Separator if minutes are also visible.
(?P<second_sign>-)?
(?P<second>[0-9.]+) # Seconds value
{second} # Second symbol (to be replaced by format.)
)?
\$ # Mathtext
'''
)
DMS_RE = re.compile(_MS_RE.format(degree=re.escape(FormatterDMS.deg_mark),
minute=re.escape(FormatterDMS.min_mark),
second=re.escape(FormatterDMS.sec_mark)),
re.VERBOSE)
HMS_RE = re.compile(_MS_RE.format(degree=re.escape(FormatterHMS.deg_mark),
minute=re.escape(FormatterHMS.min_mark),
second=re.escape(FormatterHMS.sec_mark)),
re.VERBOSE)
def dms2float(degrees, minutes=0, seconds=0):
return degrees + minutes / 60.0 + seconds / 3600.0
@pytest.mark.parametrize('args, kwargs, expected_levels, expected_factor', [
((-180, 180, 10), {'hour': False}, np.arange(-180, 181, 30), 1.0),
((-12, 12, 10), {'hour': True}, np.arange(-12, 13, 2), 1.0)
])
def test_select_step(args, kwargs, expected_levels, expected_factor):
levels, n, factor = select_step(*args, **kwargs)
assert n == len(levels)
np.testing.assert_array_equal(levels, expected_levels)
assert factor == expected_factor
@pytest.mark.parametrize('args, kwargs, expected_levels, expected_factor', [
((-180, 180, 10), {}, np.arange(-180, 181, 30), 1.0),
((-12, 12, 10), {}, np.arange(-750, 751, 150), 60.0)
])
def test_select_step24(args, kwargs, expected_levels, expected_factor):
levels, n, factor = select_step24(*args, **kwargs)
assert n == len(levels)
np.testing.assert_array_equal(levels, expected_levels)
assert factor == expected_factor
@pytest.mark.parametrize('args, kwargs, expected_levels, expected_factor', [
((dms2float(20, 21.2), dms2float(21, 33.3), 5), {},
np.arange(1215, 1306, 15), 60.0),
((dms2float(20.5, seconds=21.2), dms2float(20.5, seconds=33.3), 5), {},
np.arange(73820, 73835, 2), 3600.0),
((dms2float(20, 21.2), dms2float(20, 53.3), 5), {},
np.arange(1220, 1256, 5), 60.0),
((21.2, 33.3, 5), {},
np.arange(20, 35, 2), 1.0),
((dms2float(20, 21.2), dms2float(21, 33.3), 5), {},
np.arange(1215, 1306, 15), 60.0),
((dms2float(20.5, seconds=21.2), dms2float(20.5, seconds=33.3), 5), {},
np.arange(73820, 73835, 2), 3600.0),
((dms2float(20.5, seconds=21.2), dms2float(20.5, seconds=21.4), 5), {},
np.arange(7382120, 7382141, 5), 360000.0),
# test threshold factor
((dms2float(20.5, seconds=11.2), dms2float(20.5, seconds=53.3), 5),
{'threshold_factor': 60}, np.arange(12301, 12310), 600.0),
((dms2float(20.5, seconds=11.2), dms2float(20.5, seconds=53.3), 5),
{'threshold_factor': 1}, np.arange(20502, 20517, 2), 1000.0),
])
def test_select_step360(args, kwargs, expected_levels, expected_factor):
levels, n, factor = select_step360(*args, **kwargs)
assert n == len(levels)
np.testing.assert_array_equal(levels, expected_levels)
assert factor == expected_factor
@pytest.mark.parametrize('Formatter, regex',
[(FormatterDMS, DMS_RE),
(FormatterHMS, HMS_RE)],
ids=['Degree/Minute/Second', 'Hour/Minute/Second'])
@pytest.mark.parametrize('direction, factor, values', [
("left", 60, [0, -30, -60]),
("left", 600, [12301, 12302, 12303]),
("left", 3600, [0, -30, -60]),
("left", 36000, [738210, 738215, 738220]),
("left", 360000, [7382120, 7382125, 7382130]),
("left", 1., [45, 46, 47]),
("left", 10., [452, 453, 454]),
])
def test_formatters(Formatter, regex, direction, factor, values):
fmt = Formatter()
result = fmt(direction, factor, values)
prev_degree = prev_minute = prev_second = None
for tick, value in zip(result, values):
m = regex.match(tick)
assert m is not None, f'{tick!r} is not an expected tick format.'
sign = sum(m.group(sign + '_sign') is not None
for sign in ('degree', 'minute', 'second'))
assert sign <= 1, f'Only one element of tick {tick!r} may have a sign.'
sign = 1 if sign == 0 else -1
degree = float(m.group('degree') or prev_degree or 0)
minute = float(m.group('minute') or prev_minute or 0)
second = float(m.group('second') or prev_second or 0)
if Formatter == FormatterHMS:
# 360 degrees as plot range -> 24 hours as labelled range
expected_value = pytest.approx((value // 15) / factor)
else:
expected_value = pytest.approx(value / factor)
assert sign * dms2float(degree, minute, second) == expected_value, \
f'{tick!r} does not match expected tick value.'
prev_degree = degree
prev_minute = minute
prev_second = second

View File

@ -0,0 +1,99 @@
import matplotlib.pyplot as plt
from matplotlib.testing.decorators import image_comparison
from mpl_toolkits.axisartist import AxisArtistHelperRectlinear
from mpl_toolkits.axisartist.axis_artist import (AxisArtist, AxisLabel,
LabelBase, Ticks, TickLabels)
@image_comparison(['axis_artist_ticks.png'], style='default')
def test_ticks():
fig, ax = plt.subplots()
ax.xaxis.set_visible(False)
ax.yaxis.set_visible(False)
locs_angles = [((i / 10, 0.0), i * 30) for i in range(-1, 12)]
ticks_in = Ticks(ticksize=10, axis=ax.xaxis)
ticks_in.set_locs_angles(locs_angles)
ax.add_artist(ticks_in)
ticks_out = Ticks(ticksize=10, tick_out=True, color='C3', axis=ax.xaxis)
ticks_out.set_locs_angles(locs_angles)
ax.add_artist(ticks_out)
@image_comparison(['axis_artist_labelbase.png'], style='default')
def test_labelbase():
# Remove this line when this test image is regenerated.
plt.rcParams['text.kerning_factor'] = 6
fig, ax = plt.subplots()
ax.plot([0.5], [0.5], "o")
label = LabelBase(0.5, 0.5, "Test")
label._ref_angle = -90
label._offset_radius = 50
label.set_rotation(-90)
label.set(ha="center", va="top")
ax.add_artist(label)
@image_comparison(['axis_artist_ticklabels.png'], style='default')
def test_ticklabels():
# Remove this line when this test image is regenerated.
plt.rcParams['text.kerning_factor'] = 6
fig, ax = plt.subplots()
ax.xaxis.set_visible(False)
ax.yaxis.set_visible(False)
ax.plot([0.2, 0.4], [0.5, 0.5], "o")
ticks = Ticks(ticksize=10, axis=ax.xaxis)
ax.add_artist(ticks)
locs_angles_labels = [((0.2, 0.5), -90, "0.2"),
((0.4, 0.5), -120, "0.4")]
tick_locs_angles = [(xy, a + 180) for xy, a, l in locs_angles_labels]
ticks.set_locs_angles(tick_locs_angles)
ticklabels = TickLabels(axis_direction="left")
ticklabels._locs_angles_labels = locs_angles_labels
ticklabels.set_pad(10)
ax.add_artist(ticklabels)
ax.plot([0.5], [0.5], "s")
axislabel = AxisLabel(0.5, 0.5, "Test")
axislabel._offset_radius = 20
axislabel._ref_angle = 0
axislabel.set_axis_direction("bottom")
ax.add_artist(axislabel)
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
@image_comparison(['axis_artist.png'], style='default')
def test_axis_artist():
# Remove this line when this test image is regenerated.
plt.rcParams['text.kerning_factor'] = 6
fig, ax = plt.subplots()
ax.xaxis.set_visible(False)
ax.yaxis.set_visible(False)
for loc in ('left', 'right', 'bottom'):
helper = AxisArtistHelperRectlinear.Fixed(ax, loc=loc)
axisline = AxisArtist(ax, helper, offset=None, axis_direction=loc)
ax.add_artist(axisline)
# Settings for bottom AxisArtist.
axisline.set_label("TTT")
axisline.major_ticks.set_tick_out(False)
axisline.label.set_pad(5)
ax.set_ylabel("Test")

View File

@ -0,0 +1,147 @@
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.testing.decorators import image_comparison
from matplotlib.transforms import IdentityTransform
from mpl_toolkits.axisartist.axislines import AxesZero, SubplotZero, Subplot
from mpl_toolkits.axisartist import Axes, SubplotHost
@image_comparison(['SubplotZero.png'], style='default')
def test_SubplotZero():
# Remove this line when this test image is regenerated.
plt.rcParams['text.kerning_factor'] = 6
fig = plt.figure()
ax = SubplotZero(fig, 1, 1, 1)
fig.add_subplot(ax)
ax.axis["xzero"].set_visible(True)
ax.axis["xzero"].label.set_text("Axis Zero")
for n in ["top", "right"]:
ax.axis[n].set_visible(False)
xx = np.arange(0, 2 * np.pi, 0.01)
ax.plot(xx, np.sin(xx))
ax.set_ylabel("Test")
@image_comparison(['Subplot.png'], style='default')
def test_Subplot():
# Remove this line when this test image is regenerated.
plt.rcParams['text.kerning_factor'] = 6
fig = plt.figure()
ax = Subplot(fig, 1, 1, 1)
fig.add_subplot(ax)
xx = np.arange(0, 2 * np.pi, 0.01)
ax.plot(xx, np.sin(xx))
ax.set_ylabel("Test")
ax.axis["top"].major_ticks.set_tick_out(True)
ax.axis["bottom"].major_ticks.set_tick_out(True)
ax.axis["bottom"].set_label("Tk0")
def test_Axes():
fig = plt.figure()
ax = Axes(fig, [0.15, 0.1, 0.65, 0.8])
fig.add_axes(ax)
ax.plot([1, 2, 3], [0, 1, 2])
ax.set_xscale('log')
fig.canvas.draw()
@image_comparison(['ParasiteAxesAuxTrans_meshplot.png'],
remove_text=True, style='default', tol=0.075)
def test_ParasiteAxesAuxTrans():
data = np.ones((6, 6))
data[2, 2] = 2
data[0, :] = 0
data[-2, :] = 0
data[:, 0] = 0
data[:, -2] = 0
x = np.arange(6)
y = np.arange(6)
xx, yy = np.meshgrid(x, y)
funcnames = ['pcolor', 'pcolormesh', 'contourf']
fig = plt.figure()
for i, name in enumerate(funcnames):
ax1 = SubplotHost(fig, 1, 3, i+1)
fig.add_subplot(ax1)
ax2 = ax1.get_aux_axes(IdentityTransform(), viewlim_mode=None)
if name.startswith('pcolor'):
getattr(ax2, name)(xx, yy, data[:-1, :-1])
else:
getattr(ax2, name)(xx, yy, data)
ax1.set_xlim((0, 5))
ax1.set_ylim((0, 5))
ax2.contour(xx, yy, data, colors='k')
@image_comparison(['axisline_style.png'], remove_text=True, style='mpl20')
def test_axisline_style():
fig = plt.figure(figsize=(2, 2))
ax = fig.add_subplot(axes_class=AxesZero)
ax.axis["xzero"].set_axisline_style("-|>")
ax.axis["xzero"].set_visible(True)
ax.axis["yzero"].set_axisline_style("->")
ax.axis["yzero"].set_visible(True)
for direction in ("left", "right", "bottom", "top"):
ax.axis[direction].set_visible(False)
@image_comparison(['axisline_style_size_color.png'], remove_text=True,
style='mpl20')
def test_axisline_style_size_color():
fig = plt.figure(figsize=(2, 2))
ax = fig.add_subplot(axes_class=AxesZero)
ax.axis["xzero"].set_axisline_style("-|>", size=2.0, facecolor='r')
ax.axis["xzero"].set_visible(True)
ax.axis["yzero"].set_axisline_style("->, size=1.5")
ax.axis["yzero"].set_visible(True)
for direction in ("left", "right", "bottom", "top"):
ax.axis[direction].set_visible(False)
@image_comparison(['axisline_style_tight.png'], remove_text=True,
style='mpl20')
def test_axisline_style_tight():
fig = plt.figure(figsize=(2, 2))
ax = fig.add_subplot(axes_class=AxesZero)
ax.axis["xzero"].set_axisline_style("-|>", size=5, facecolor='g')
ax.axis["xzero"].set_visible(True)
ax.axis["yzero"].set_axisline_style("->, size=8")
ax.axis["yzero"].set_visible(True)
for direction in ("left", "right", "bottom", "top"):
ax.axis[direction].set_visible(False)
fig.tight_layout()
@image_comparison(['subplotzero_ylabel.png'], style='mpl20')
def test_subplotzero_ylabel():
fig = plt.figure()
ax = fig.add_subplot(111, axes_class=SubplotZero)
ax.set(xlim=(-3, 7), ylim=(-3, 7), xlabel="x", ylabel="y")
zero_axis = ax.axis["xzero", "yzero"]
zero_axis.set_visible(True) # they are hidden by default
ax.axis["left", "right", "bottom", "top"].set_visible(False)
zero_axis.set_axisline_style("->")

View File

@ -0,0 +1,115 @@
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.projections as mprojections
import matplotlib.transforms as mtransforms
from matplotlib.testing.decorators import image_comparison
from mpl_toolkits.axisartist.axislines import Subplot
from mpl_toolkits.axisartist.floating_axes import (
FloatingAxes, GridHelperCurveLinear)
from mpl_toolkits.axisartist.grid_finder import FixedLocator
from mpl_toolkits.axisartist import angle_helper
def test_subplot():
fig = plt.figure(figsize=(5, 5))
ax = Subplot(fig, 111)
fig.add_subplot(ax)
# Rather high tolerance to allow ongoing work with floating axes internals;
# remove when image is regenerated.
@image_comparison(['curvelinear3.png'], style='default', tol=5)
def test_curvelinear3():
fig = plt.figure(figsize=(5, 5))
tr = (mtransforms.Affine2D().scale(np.pi / 180, 1) +
mprojections.PolarAxes.PolarTransform(apply_theta_transforms=False))
grid_helper = GridHelperCurveLinear(
tr,
extremes=(0, 360, 10, 3),
grid_locator1=angle_helper.LocatorDMS(15),
grid_locator2=FixedLocator([2, 4, 6, 8, 10]),
tick_formatter1=angle_helper.FormatterDMS(),
tick_formatter2=None)
ax1 = fig.add_subplot(axes_class=FloatingAxes, grid_helper=grid_helper)
r_scale = 10
tr2 = mtransforms.Affine2D().scale(1, 1 / r_scale) + tr
grid_helper2 = GridHelperCurveLinear(
tr2,
extremes=(0, 360, 10 * r_scale, 3 * r_scale),
grid_locator2=FixedLocator([30, 60, 90]))
ax1.axis["right"] = axis = grid_helper2.new_fixed_axis("right", axes=ax1)
ax1.axis["left"].label.set_text("Test 1")
ax1.axis["right"].label.set_text("Test 2")
ax1.axis["left", "right"].set_visible(False)
axis = grid_helper.new_floating_axis(1, 7, axes=ax1,
axis_direction="bottom")
ax1.axis["z"] = axis
axis.toggle(all=True, label=True)
axis.label.set_text("z = ?")
axis.label.set_visible(True)
axis.line.set_color("0.5")
ax2 = ax1.get_aux_axes(tr)
xx, yy = [67, 90, 75, 30], [2, 5, 8, 4]
ax2.scatter(xx, yy)
l, = ax2.plot(xx, yy, "k-")
l.set_clip_path(ax1.patch)
# Rather high tolerance to allow ongoing work with floating axes internals;
# remove when image is regenerated.
@image_comparison(['curvelinear4.png'], style='default', tol=0.9)
def test_curvelinear4():
# Remove this line when this test image is regenerated.
plt.rcParams['text.kerning_factor'] = 6
fig = plt.figure(figsize=(5, 5))
tr = (mtransforms.Affine2D().scale(np.pi / 180, 1) +
mprojections.PolarAxes.PolarTransform(apply_theta_transforms=False))
grid_helper = GridHelperCurveLinear(
tr,
extremes=(120, 30, 10, 0),
grid_locator1=angle_helper.LocatorDMS(5),
grid_locator2=FixedLocator([2, 4, 6, 8, 10]),
tick_formatter1=angle_helper.FormatterDMS(),
tick_formatter2=None)
ax1 = fig.add_subplot(axes_class=FloatingAxes, grid_helper=grid_helper)
ax1.clear() # Check that clear() also restores the correct limits on ax1.
ax1.axis["left"].label.set_text("Test 1")
ax1.axis["right"].label.set_text("Test 2")
ax1.axis["top"].set_visible(False)
axis = grid_helper.new_floating_axis(1, 70, axes=ax1,
axis_direction="bottom")
ax1.axis["z"] = axis
axis.toggle(all=True, label=True)
axis.label.set_axis_direction("top")
axis.label.set_text("z = ?")
axis.label.set_visible(True)
axis.line.set_color("0.5")
ax2 = ax1.get_aux_axes(tr)
xx, yy = [67, 90, 75, 30], [2, 5, 8, 4]
ax2.scatter(xx, yy)
l, = ax2.plot(xx, yy, "k-")
l.set_clip_path(ax1.patch)
def test_axis_direction():
# Check that axis direction is propagated on a floating axis
fig = plt.figure()
ax = Subplot(fig, 111)
fig.add_subplot(ax)
ax.axis['y'] = ax.new_floating_axis(nth_coord=1, value=0,
axis_direction='left')
assert ax.axis['y']._axis_direction == 'left'

View File

@ -0,0 +1,34 @@
import numpy as np
import pytest
from matplotlib.transforms import Bbox
from mpl_toolkits.axisartist.grid_finder import (
_find_line_box_crossings, FormatterPrettyPrint, MaxNLocator)
def test_find_line_box_crossings():
x = np.array([-3, -2, -1, 0., 1, 2, 3, 2, 1, 0, -1, -2, -3, 5])
y = np.arange(len(x))
bbox = Bbox.from_extents(-2, 3, 2, 12.5)
left, right, bottom, top = _find_line_box_crossings(
np.column_stack([x, y]), bbox)
((lx0, ly0), la0), ((lx1, ly1), la1), = left
((rx0, ry0), ra0), ((rx1, ry1), ra1), = right
((bx0, by0), ba0), = bottom
((tx0, ty0), ta0), = top
assert (lx0, ly0, la0) == (-2, 11, 135)
assert (lx1, ly1, la1) == pytest.approx((-2., 12.125, 7.125016))
assert (rx0, ry0, ra0) == (2, 5, 45)
assert (rx1, ry1, ra1) == (2, 7, 135)
assert (bx0, by0, ba0) == (0, 3, 45)
assert (tx0, ty0, ta0) == pytest.approx((1., 12.5, 7.125016))
def test_pretty_print_format():
locator = MaxNLocator()
locs, nloc, factor = locator(0, 100)
fmt = FormatterPrettyPrint()
assert fmt("left", None, locs) == \
[r'$\mathdefault{%d}$' % (l, ) for l in locs]

View File

@ -0,0 +1,207 @@
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.path import Path
from matplotlib.projections import PolarAxes
from matplotlib.ticker import FuncFormatter
from matplotlib.transforms import Affine2D, Transform
from matplotlib.testing.decorators import image_comparison
from mpl_toolkits.axisartist import SubplotHost
from mpl_toolkits.axes_grid1.parasite_axes import host_axes_class_factory
from mpl_toolkits.axisartist import angle_helper
from mpl_toolkits.axisartist.axislines import Axes
from mpl_toolkits.axisartist.grid_helper_curvelinear import \
GridHelperCurveLinear
@image_comparison(['custom_transform.png'], style='default', tol=0.2)
def test_custom_transform():
class MyTransform(Transform):
input_dims = output_dims = 2
def __init__(self, resolution):
"""
Resolution is the number of steps to interpolate between each input
line segment to approximate its path in transformed space.
"""
Transform.__init__(self)
self._resolution = resolution
def transform(self, ll):
x, y = ll.T
return np.column_stack([x, y - x])
transform_non_affine = transform
def transform_path(self, path):
ipath = path.interpolated(self._resolution)
return Path(self.transform(ipath.vertices), ipath.codes)
transform_path_non_affine = transform_path
def inverted(self):
return MyTransformInv(self._resolution)
class MyTransformInv(Transform):
input_dims = output_dims = 2
def __init__(self, resolution):
Transform.__init__(self)
self._resolution = resolution
def transform(self, ll):
x, y = ll.T
return np.column_stack([x, y + x])
def inverted(self):
return MyTransform(self._resolution)
fig = plt.figure()
SubplotHost = host_axes_class_factory(Axes)
tr = MyTransform(1)
grid_helper = GridHelperCurveLinear(tr)
ax1 = SubplotHost(fig, 1, 1, 1, grid_helper=grid_helper)
fig.add_subplot(ax1)
ax2 = ax1.get_aux_axes(tr, viewlim_mode="equal")
ax2.plot([3, 6], [5.0, 10.])
ax1.set_aspect(1.)
ax1.set_xlim(0, 10)
ax1.set_ylim(0, 10)
ax1.grid(True)
@image_comparison(['polar_box.png'], style='default', tol=0.04)
def test_polar_box():
fig = plt.figure(figsize=(5, 5))
# PolarAxes.PolarTransform takes radian. However, we want our coordinate
# system in degree
tr = (Affine2D().scale(np.pi / 180., 1.) +
PolarAxes.PolarTransform(apply_theta_transforms=False))
# polar projection, which involves cycle, and also has limits in
# its coordinates, needs a special method to find the extremes
# (min, max of the coordinate within the view).
extreme_finder = angle_helper.ExtremeFinderCycle(20, 20,
lon_cycle=360,
lat_cycle=None,
lon_minmax=None,
lat_minmax=(0, np.inf))
grid_helper = GridHelperCurveLinear(
tr,
extreme_finder=extreme_finder,
grid_locator1=angle_helper.LocatorDMS(12),
tick_formatter1=angle_helper.FormatterDMS(),
tick_formatter2=FuncFormatter(lambda x, p: "eight" if x == 8 else f"{int(x)}"),
)
ax1 = SubplotHost(fig, 1, 1, 1, grid_helper=grid_helper)
ax1.axis["right"].major_ticklabels.set_visible(True)
ax1.axis["top"].major_ticklabels.set_visible(True)
# let right axis shows ticklabels for 1st coordinate (angle)
ax1.axis["right"].get_helper().nth_coord_ticks = 0
# let bottom axis shows ticklabels for 2nd coordinate (radius)
ax1.axis["bottom"].get_helper().nth_coord_ticks = 1
fig.add_subplot(ax1)
ax1.axis["lat"] = axis = grid_helper.new_floating_axis(0, 45, axes=ax1)
axis.label.set_text("Test")
axis.label.set_visible(True)
axis.get_helper().set_extremes(2, 12)
ax1.axis["lon"] = axis = grid_helper.new_floating_axis(1, 6, axes=ax1)
axis.label.set_text("Test 2")
axis.get_helper().set_extremes(-180, 90)
# A parasite axes with given transform
ax2 = ax1.get_aux_axes(tr, viewlim_mode="equal")
assert ax2.transData == tr + ax1.transData
# Anything you draw in ax2 will match the ticks and grids of ax1.
ax2.plot(np.linspace(0, 30, 50), np.linspace(10, 10, 50))
ax1.set_aspect(1.)
ax1.set_xlim(-5, 12)
ax1.set_ylim(-5, 10)
ax1.grid(True)
# Remove tol & kerning_factor when this test image is regenerated.
@image_comparison(['axis_direction.png'], style='default', tol=0.13)
def test_axis_direction():
plt.rcParams['text.kerning_factor'] = 6
fig = plt.figure(figsize=(5, 5))
# PolarAxes.PolarTransform takes radian. However, we want our coordinate
# system in degree
tr = (Affine2D().scale(np.pi / 180., 1.) +
PolarAxes.PolarTransform(apply_theta_transforms=False))
# polar projection, which involves cycle, and also has limits in
# its coordinates, needs a special method to find the extremes
# (min, max of the coordinate within the view).
# 20, 20 : number of sampling points along x, y direction
extreme_finder = angle_helper.ExtremeFinderCycle(20, 20,
lon_cycle=360,
lat_cycle=None,
lon_minmax=None,
lat_minmax=(0, np.inf),
)
grid_locator1 = angle_helper.LocatorDMS(12)
tick_formatter1 = angle_helper.FormatterDMS()
grid_helper = GridHelperCurveLinear(tr,
extreme_finder=extreme_finder,
grid_locator1=grid_locator1,
tick_formatter1=tick_formatter1)
ax1 = SubplotHost(fig, 1, 1, 1, grid_helper=grid_helper)
for axis in ax1.axis.values():
axis.set_visible(False)
fig.add_subplot(ax1)
ax1.axis["lat1"] = axis = grid_helper.new_floating_axis(
0, 130,
axes=ax1, axis_direction="left")
axis.label.set_text("Test")
axis.label.set_visible(True)
axis.get_helper().set_extremes(0.001, 10)
ax1.axis["lat2"] = axis = grid_helper.new_floating_axis(
0, 50,
axes=ax1, axis_direction="right")
axis.label.set_text("Test")
axis.label.set_visible(True)
axis.get_helper().set_extremes(0.001, 10)
ax1.axis["lon"] = axis = grid_helper.new_floating_axis(
1, 10,
axes=ax1, axis_direction="bottom")
axis.label.set_text("Test 2")
axis.get_helper().set_extremes(50, 130)
axis.major_ticklabels.set_axis_direction("top")
axis.label.set_axis_direction("top")
grid_helper.grid_finder.grid_locator1.set_params(nbins=5)
grid_helper.grid_finder.grid_locator2.set_params(nbins=5)
ax1.set_aspect(1.)
ax1.set_xlim(-8, 8)
ax1.set_ylim(-4, 12)
ax1.grid(True)