up follow livre
This commit is contained in:
parent
b4b4398bb0
commit
3a7a3849ae
12242 changed files with 2564461 additions and 6914 deletions
|
|
@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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, SubplotDivider, AxesDivider, make_axes_locatable)
|
||||
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,479 @@
|
|||
"""
|
||||
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):
|
||||
|
||||
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,275 @@
|
|||
"""
|
||||
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)
|
||||
|
||||
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.get_figure(root=False))
|
||||
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.')
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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,145 @@
|
|||
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), layout='tight')
|
||||
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)
|
||||
|
||||
|
||||
@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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue