asd
This commit is contained in:
@ -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
|
||||
@ -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
|
||||
@ -0,0 +1,2 @@
|
||||
from mpl_toolkits.axes_grid1.axes_divider import ( # noqa
|
||||
Divider, AxesLocator, SubplotDivider, AxesDivider, make_axes_locatable)
|
||||
@ -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
|
||||
@ -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
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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)]
|
||||
@ -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, ""
|
||||
@ -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)
|
||||
@ -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.')
|
||||
@ -0,0 +1,2 @@
|
||||
from matplotlib.testing.conftest import (mpl_test_settings, # noqa
|
||||
pytest_configure, pytest_unconfigure)
|
||||
@ -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
|
||||
@ -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")
|
||||
@ -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("->")
|
||||
@ -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'
|
||||
@ -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]
|
||||
@ -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)
|
||||
Reference in New Issue
Block a user