aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/matplotlib/py3/mpl_toolkits/axisartist
diff options
context:
space:
mode:
authorshumkovnd <shumkovnd@yandex-team.com>2023-11-10 14:39:34 +0300
committershumkovnd <shumkovnd@yandex-team.com>2023-11-10 16:42:24 +0300
commit77eb2d3fdcec5c978c64e025ced2764c57c00285 (patch)
treec51edb0748ca8d4a08d7c7323312c27ba1a8b79a /contrib/python/matplotlib/py3/mpl_toolkits/axisartist
parentdd6d20cadb65582270ac23f4b3b14ae189704b9d (diff)
downloadydb-77eb2d3fdcec5c978c64e025ced2764c57c00285.tar.gz
KIKIMR-19287: add task_stats_drawing script
Diffstat (limited to 'contrib/python/matplotlib/py3/mpl_toolkits/axisartist')
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/__init__.py13
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/angle_helper.py394
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_divider.py2
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_grid.py23
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_rgb.py18
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axis_artist.py1115
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axisline_style.py193
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axislines.py531
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/floating_axes.py298
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/grid_finder.py335
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/grid_helper_curvelinear.py336
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/parasite_axes.py7
12 files changed, 3265 insertions, 0 deletions
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/__init__.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/__init__.py
new file mode 100644
index 0000000000..47242cf7f0
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/__init__.py
@@ -0,0 +1,13 @@
+from .axislines import (
+ Axes, AxesZero, AxisArtistHelper, AxisArtistHelperRectlinear,
+ GridHelperBase, GridHelperRectlinear, Subplot, SubplotZero)
+from .axis_artist import AxisArtist, GridlinesCollection
+from .grid_helper_curvelinear import GridHelperCurveLinear
+from .floating_axes import FloatingAxes, FloatingSubplot
+from mpl_toolkits.axes_grid1.parasite_axes import (
+ host_axes_class_factory, parasite_axes_class_factory)
+
+
+ParasiteAxes = parasite_axes_class_factory(Axes)
+HostAxes = host_axes_class_factory(Axes)
+SubplotHost = HostAxes
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/angle_helper.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/angle_helper.py
new file mode 100644
index 0000000000..1786cd70bc
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/angle_helper.py
@@ -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
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_divider.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_divider.py
new file mode 100644
index 0000000000..a01d4e27df
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_divider.py
@@ -0,0 +1,2 @@
+from mpl_toolkits.axes_grid1.axes_divider import ( # noqa
+ Divider, AxesLocator, SubplotDivider, AxesDivider, make_axes_locatable)
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_grid.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_grid.py
new file mode 100644
index 0000000000..ecb3e9d92c
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_grid.py
@@ -0,0 +1,23 @@
+from matplotlib import _api
+
+import mpl_toolkits.axes_grid1.axes_grid as axes_grid_orig
+from .axislines import Axes
+
+
+_api.warn_deprecated(
+ "3.8", name=__name__, obj_type="module", alternative="axes_grid1.axes_grid")
+
+
+@_api.deprecated("3.8", alternative=(
+ "axes_grid1.axes_grid.Grid(..., axes_class=axislines.Axes"))
+class Grid(axes_grid_orig.Grid):
+ _defaultAxesClass = Axes
+
+
+@_api.deprecated("3.8", alternative=(
+ "axes_grid1.axes_grid.ImageGrid(..., axes_class=axislines.Axes"))
+class ImageGrid(axes_grid_orig.ImageGrid):
+ _defaultAxesClass = Axes
+
+
+AxesGrid = ImageGrid
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_rgb.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_rgb.py
new file mode 100644
index 0000000000..2195747469
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_rgb.py
@@ -0,0 +1,18 @@
+from matplotlib import _api
+from mpl_toolkits.axes_grid1.axes_rgb import ( # noqa
+ make_rgb_axes, RGBAxes as _RGBAxes)
+from .axislines import Axes
+
+
+_api.warn_deprecated(
+ "3.8", name=__name__, obj_type="module", alternative="axes_grid1.axes_rgb")
+
+
+@_api.deprecated("3.8", alternative=(
+ "axes_grid1.axes_rgb.RGBAxes(..., axes_class=axislines.Axes"))
+class RGBAxes(_RGBAxes):
+ """
+ Subclass of `~.axes_grid1.axes_rgb.RGBAxes` with
+ ``_defaultAxesClass`` = `.axislines.Axes`.
+ """
+ _defaultAxesClass = Axes
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axis_artist.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axis_artist.py
new file mode 100644
index 0000000000..407ad07a3d
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axis_artist.py
@@ -0,0 +1,1115 @@
+"""
+The :mod:`.axis_artist` module implements custom artists to draw axis elements
+(axis lines and labels, tick lines and labels, grid lines).
+
+Axis lines and labels and tick lines and labels are managed by the `AxisArtist`
+class; grid lines are managed by the `GridlinesCollection` class.
+
+There is one `AxisArtist` per Axis; it can be accessed through
+the ``axis`` dictionary of the parent Axes (which should be a
+`mpl_toolkits.axislines.Axes`), e.g. ``ax.axis["bottom"]``.
+
+Children of the AxisArtist are accessed as attributes: ``.line`` and ``.label``
+for the axis line and label, ``.major_ticks``, ``.major_ticklabels``,
+``.minor_ticks``, ``.minor_ticklabels`` for the tick lines and labels (e.g.
+``ax.axis["bottom"].line``).
+
+Children properties (colors, fonts, line widths, etc.) can be set using
+setters, e.g. ::
+
+ # Make the major ticks of the bottom axis red.
+ ax.axis["bottom"].major_ticks.set_color("red")
+
+However, things like the locations of ticks, and their ticklabels need to be
+changed from the side of the grid_helper.
+
+axis_direction
+--------------
+
+`AxisArtist`, `AxisLabel`, `TickLabels` have an *axis_direction* attribute,
+which adjusts the location, angle, etc. The *axis_direction* must be one of
+"left", "right", "bottom", "top", and follows the Matplotlib convention for
+rectangular axis.
+
+For example, for the *bottom* axis (the left and right is relative to the
+direction of the increasing coordinate),
+
+* ticklabels and axislabel are on the right
+* ticklabels and axislabel have text angle of 0
+* ticklabels are baseline, center-aligned
+* axislabel is top, center-aligned
+
+The text angles are actually relative to (90 + angle of the direction to the
+ticklabel), which gives 0 for bottom axis.
+
+=================== ====== ======== ====== ========
+Property left bottom right top
+=================== ====== ======== ====== ========
+ticklabel location left right right left
+axislabel location left right right left
+ticklabel angle 90 0 -90 180
+axislabel angle 180 0 0 180
+ticklabel va center baseline center baseline
+axislabel va center top center bottom
+ticklabel ha right center right center
+axislabel ha right center right center
+=================== ====== ======== ====== ========
+
+Ticks are by default direct opposite side of the ticklabels. To make ticks to
+the same side of the ticklabels, ::
+
+ ax.axis["bottom"].major_ticks.set_tick_out(True)
+
+The following attributes can be customized (use the ``set_xxx`` methods):
+
+* `Ticks`: ticksize, tick_out
+* `TickLabels`: pad
+* `AxisLabel`: pad
+"""
+
+# FIXME :
+# angles are given in data coordinate - need to convert it to canvas coordinate
+
+
+from operator import methodcaller
+
+import numpy as np
+
+import matplotlib as mpl
+from matplotlib import _api, cbook
+import matplotlib.artist as martist
+import matplotlib.colors as mcolors
+import matplotlib.text as mtext
+from matplotlib.collections import LineCollection
+from matplotlib.lines import Line2D
+from matplotlib.patches import PathPatch
+from matplotlib.path import Path
+from matplotlib.transforms import (
+ Affine2D, Bbox, IdentityTransform, ScaledTranslation)
+
+from .axisline_style import AxislineStyle
+
+
+class AttributeCopier:
+ def get_ref_artist(self):
+ """
+ Return the underlying artist that actually defines some properties
+ (e.g., color) of this artist.
+ """
+ raise RuntimeError("get_ref_artist must overridden")
+
+ def get_attribute_from_ref_artist(self, attr_name):
+ getter = methodcaller("get_" + attr_name)
+ prop = getter(super())
+ return getter(self.get_ref_artist()) if prop == "auto" else prop
+
+
+class Ticks(AttributeCopier, Line2D):
+ """
+ Ticks are derived from `.Line2D`, and note that ticks themselves
+ are markers. Thus, you should use set_mec, set_mew, etc.
+
+ To change the tick size (length), you need to use
+ `set_ticksize`. To change the direction of the ticks (ticks are
+ in opposite direction of ticklabels by default), use
+ ``set_tick_out(False)``
+ """
+
+ def __init__(self, ticksize, tick_out=False, *, axis=None, **kwargs):
+ self._ticksize = ticksize
+ self.locs_angles_labels = []
+
+ self.set_tick_out(tick_out)
+
+ self._axis = axis
+ if self._axis is not None:
+ if "color" not in kwargs:
+ kwargs["color"] = "auto"
+ if "mew" not in kwargs and "markeredgewidth" not in kwargs:
+ kwargs["markeredgewidth"] = "auto"
+
+ Line2D.__init__(self, [0.], [0.], **kwargs)
+ self.set_snap(True)
+
+ def get_ref_artist(self):
+ # docstring inherited
+ return self._axis.majorTicks[0].tick1line
+
+ def set_color(self, color):
+ # docstring inherited
+ # Unlike the base Line2D.set_color, this also supports "auto".
+ if not cbook._str_equal(color, "auto"):
+ mcolors._check_color_like(color=color)
+ self._color = color
+ self.stale = True
+
+ def get_color(self):
+ return self.get_attribute_from_ref_artist("color")
+
+ def get_markeredgecolor(self):
+ return self.get_attribute_from_ref_artist("markeredgecolor")
+
+ def get_markeredgewidth(self):
+ return self.get_attribute_from_ref_artist("markeredgewidth")
+
+ def set_tick_out(self, b):
+ """Set whether ticks are drawn inside or outside the axes."""
+ self._tick_out = b
+
+ def get_tick_out(self):
+ """Return whether ticks are drawn inside or outside the axes."""
+ return self._tick_out
+
+ def set_ticksize(self, ticksize):
+ """Set length of the ticks in points."""
+ self._ticksize = ticksize
+
+ def get_ticksize(self):
+ """Return length of the ticks in points."""
+ return self._ticksize
+
+ def set_locs_angles(self, locs_angles):
+ self.locs_angles = locs_angles
+
+ _tickvert_path = Path([[0., 0.], [1., 0.]])
+
+ def draw(self, renderer):
+ if not self.get_visible():
+ return
+
+ gc = renderer.new_gc()
+ gc.set_foreground(self.get_markeredgecolor())
+ gc.set_linewidth(self.get_markeredgewidth())
+ gc.set_alpha(self._alpha)
+
+ path_trans = self.get_transform()
+ marker_transform = (Affine2D()
+ .scale(renderer.points_to_pixels(self._ticksize)))
+ if self.get_tick_out():
+ marker_transform.rotate_deg(180)
+
+ for loc, angle in self.locs_angles:
+ locs = path_trans.transform_non_affine(np.array([loc]))
+ if self.axes and not self.axes.viewLim.contains(*locs[0]):
+ continue
+ renderer.draw_markers(
+ gc, self._tickvert_path,
+ marker_transform + Affine2D().rotate_deg(angle),
+ Path(locs), path_trans.get_affine())
+
+ gc.restore()
+
+
+class LabelBase(mtext.Text):
+ """
+ A base class for `.AxisLabel` and `.TickLabels`. The position and
+ angle of the text are calculated by the offset_ref_angle,
+ text_ref_angle, and offset_radius attributes.
+ """
+
+ def __init__(self, *args, **kwargs):
+ self.locs_angles_labels = []
+ self._ref_angle = 0
+ self._offset_radius = 0.
+
+ super().__init__(*args, **kwargs)
+
+ self.set_rotation_mode("anchor")
+ self._text_follow_ref_angle = True
+
+ @property
+ def _text_ref_angle(self):
+ if self._text_follow_ref_angle:
+ return self._ref_angle + 90
+ else:
+ return 0
+
+ @property
+ def _offset_ref_angle(self):
+ return self._ref_angle
+
+ _get_opposite_direction = {"left": "right",
+ "right": "left",
+ "top": "bottom",
+ "bottom": "top"}.__getitem__
+
+ def draw(self, renderer):
+ if not self.get_visible():
+ return
+
+ # save original and adjust some properties
+ tr = self.get_transform()
+ angle_orig = self.get_rotation()
+ theta = np.deg2rad(self._offset_ref_angle)
+ dd = self._offset_radius
+ dx, dy = dd * np.cos(theta), dd * np.sin(theta)
+
+ self.set_transform(tr + Affine2D().translate(dx, dy))
+ self.set_rotation(self._text_ref_angle + angle_orig)
+ super().draw(renderer)
+ # restore original properties
+ self.set_transform(tr)
+ self.set_rotation(angle_orig)
+
+ def get_window_extent(self, renderer=None):
+ if renderer is None:
+ renderer = self.figure._get_renderer()
+
+ # save original and adjust some properties
+ tr = self.get_transform()
+ angle_orig = self.get_rotation()
+ theta = np.deg2rad(self._offset_ref_angle)
+ dd = self._offset_radius
+ dx, dy = dd * np.cos(theta), dd * np.sin(theta)
+
+ self.set_transform(tr + Affine2D().translate(dx, dy))
+ self.set_rotation(self._text_ref_angle + angle_orig)
+ bbox = super().get_window_extent(renderer).frozen()
+ # restore original properties
+ self.set_transform(tr)
+ self.set_rotation(angle_orig)
+
+ return bbox
+
+
+class AxisLabel(AttributeCopier, LabelBase):
+ """
+ Axis label. Derived from `.Text`. The position of the text is updated
+ in the fly, so changing text position has no effect. Otherwise, the
+ properties can be changed as a normal `.Text`.
+
+ To change the pad between tick labels and axis label, use `set_pad`.
+ """
+
+ def __init__(self, *args, axis_direction="bottom", axis=None, **kwargs):
+ self._axis = axis
+ self._pad = 5
+ self._external_pad = 0 # in pixels
+ LabelBase.__init__(self, *args, **kwargs)
+ self.set_axis_direction(axis_direction)
+
+ def set_pad(self, pad):
+ """
+ Set the internal pad in points.
+
+ The actual pad will be the sum of the internal pad and the
+ external pad (the latter is set automatically by the `.AxisArtist`).
+
+ Parameters
+ ----------
+ pad : float
+ The internal pad in points.
+ """
+ self._pad = pad
+
+ def get_pad(self):
+ """
+ Return the internal pad in points.
+
+ See `.set_pad` for more details.
+ """
+ return self._pad
+
+ def get_ref_artist(self):
+ # docstring inherited
+ return self._axis.get_label()
+
+ def get_text(self):
+ # docstring inherited
+ t = super().get_text()
+ if t == "__from_axes__":
+ return self._axis.get_label().get_text()
+ return self._text
+
+ _default_alignments = dict(left=("bottom", "center"),
+ right=("top", "center"),
+ bottom=("top", "center"),
+ top=("bottom", "center"))
+
+ def set_default_alignment(self, d):
+ """
+ Set the default alignment. See `set_axis_direction` for details.
+
+ Parameters
+ ----------
+ d : {"left", "bottom", "right", "top"}
+ """
+ va, ha = _api.check_getitem(self._default_alignments, d=d)
+ self.set_va(va)
+ self.set_ha(ha)
+
+ _default_angles = dict(left=180,
+ right=0,
+ bottom=0,
+ top=180)
+
+ def set_default_angle(self, d):
+ """
+ Set the default angle. See `set_axis_direction` for details.
+
+ Parameters
+ ----------
+ d : {"left", "bottom", "right", "top"}
+ """
+ self.set_rotation(_api.check_getitem(self._default_angles, d=d))
+
+ def set_axis_direction(self, d):
+ """
+ Adjust the text angle and text alignment of axis label
+ according to the matplotlib convention.
+
+ ===================== ========== ========= ========== ==========
+ Property left bottom right top
+ ===================== ========== ========= ========== ==========
+ axislabel angle 180 0 0 180
+ axislabel va center top center bottom
+ axislabel ha right center right center
+ ===================== ========== ========= ========== ==========
+
+ Note that the text angles are actually relative to (90 + angle
+ of the direction to the ticklabel), which gives 0 for bottom
+ axis.
+
+ Parameters
+ ----------
+ d : {"left", "bottom", "right", "top"}
+ """
+ self.set_default_alignment(d)
+ self.set_default_angle(d)
+
+ def get_color(self):
+ return self.get_attribute_from_ref_artist("color")
+
+ def draw(self, renderer):
+ if not self.get_visible():
+ return
+
+ self._offset_radius = \
+ self._external_pad + renderer.points_to_pixels(self.get_pad())
+
+ super().draw(renderer)
+
+ def get_window_extent(self, renderer=None):
+ if renderer is None:
+ renderer = self.figure._get_renderer()
+ if not self.get_visible():
+ return
+
+ r = self._external_pad + renderer.points_to_pixels(self.get_pad())
+ self._offset_radius = r
+
+ bb = super().get_window_extent(renderer)
+
+ return bb
+
+
+class TickLabels(AxisLabel): # mtext.Text
+ """
+ Tick labels. While derived from `.Text`, this single artist draws all
+ ticklabels. As in `.AxisLabel`, the position of the text is updated
+ in the fly, so changing text position has no effect. Otherwise,
+ the properties can be changed as a normal `.Text`. Unlike the
+ ticklabels of the mainline Matplotlib, properties of a single
+ ticklabel alone cannot be modified.
+
+ To change the pad between ticks and ticklabels, use `~.AxisLabel.set_pad`.
+ """
+
+ def __init__(self, *, axis_direction="bottom", **kwargs):
+ super().__init__(**kwargs)
+ self.set_axis_direction(axis_direction)
+ self._axislabel_pad = 0
+
+ def get_ref_artist(self):
+ # docstring inherited
+ return self._axis.get_ticklabels()[0]
+
+ def set_axis_direction(self, label_direction):
+ """
+ Adjust the text angle and text alignment of ticklabels
+ according to the Matplotlib convention.
+
+ The *label_direction* must be one of [left, right, bottom, top].
+
+ ===================== ========== ========= ========== ==========
+ Property left bottom right top
+ ===================== ========== ========= ========== ==========
+ ticklabel angle 90 0 -90 180
+ ticklabel va center baseline center baseline
+ ticklabel ha right center right center
+ ===================== ========== ========= ========== ==========
+
+ Note that the text angles are actually relative to (90 + angle
+ of the direction to the ticklabel), which gives 0 for bottom
+ axis.
+
+ Parameters
+ ----------
+ label_direction : {"left", "bottom", "right", "top"}
+
+ """
+ self.set_default_alignment(label_direction)
+ self.set_default_angle(label_direction)
+ self._axis_direction = label_direction
+
+ def invert_axis_direction(self):
+ label_direction = self._get_opposite_direction(self._axis_direction)
+ self.set_axis_direction(label_direction)
+
+ def _get_ticklabels_offsets(self, renderer, label_direction):
+ """
+ Calculate the ticklabel offsets from the tick and their total heights.
+
+ The offset only takes account the offset due to the vertical alignment
+ of the ticklabels: if axis direction is bottom and va is 'top', it will
+ return 0; if va is 'baseline', it will return (height-descent).
+ """
+ whd_list = self.get_texts_widths_heights_descents(renderer)
+
+ if not whd_list:
+ return 0, 0
+
+ r = 0
+ va, ha = self.get_va(), self.get_ha()
+
+ if label_direction == "left":
+ pad = max(w for w, h, d in whd_list)
+ if ha == "left":
+ r = pad
+ elif ha == "center":
+ r = .5 * pad
+ elif label_direction == "right":
+ pad = max(w for w, h, d in whd_list)
+ if ha == "right":
+ r = pad
+ elif ha == "center":
+ r = .5 * pad
+ elif label_direction == "bottom":
+ pad = max(h for w, h, d in whd_list)
+ if va == "bottom":
+ r = pad
+ elif va == "center":
+ r = .5 * pad
+ elif va == "baseline":
+ max_ascent = max(h - d for w, h, d in whd_list)
+ max_descent = max(d for w, h, d in whd_list)
+ r = max_ascent
+ pad = max_ascent + max_descent
+ elif label_direction == "top":
+ pad = max(h for w, h, d in whd_list)
+ if va == "top":
+ r = pad
+ elif va == "center":
+ r = .5 * pad
+ elif va == "baseline":
+ max_ascent = max(h - d for w, h, d in whd_list)
+ max_descent = max(d for w, h, d in whd_list)
+ r = max_descent
+ pad = max_ascent + max_descent
+
+ # r : offset
+ # pad : total height of the ticklabels. This will be used to
+ # calculate the pad for the axislabel.
+ return r, pad
+
+ _default_alignments = dict(left=("center", "right"),
+ right=("center", "left"),
+ bottom=("baseline", "center"),
+ top=("baseline", "center"))
+
+ _default_angles = dict(left=90,
+ right=-90,
+ bottom=0,
+ top=180)
+
+ def draw(self, renderer):
+ if not self.get_visible():
+ self._axislabel_pad = self._external_pad
+ return
+
+ r, total_width = self._get_ticklabels_offsets(renderer,
+ self._axis_direction)
+
+ pad = self._external_pad + renderer.points_to_pixels(self.get_pad())
+ self._offset_radius = r + pad
+
+ for (x, y), a, l in self._locs_angles_labels:
+ if not l.strip():
+ continue
+ self._ref_angle = a
+ self.set_x(x)
+ self.set_y(y)
+ self.set_text(l)
+ LabelBase.draw(self, renderer)
+
+ # the value saved will be used to draw axislabel.
+ self._axislabel_pad = total_width + pad
+
+ def set_locs_angles_labels(self, locs_angles_labels):
+ self._locs_angles_labels = locs_angles_labels
+
+ def get_window_extents(self, renderer=None):
+ if renderer is None:
+ renderer = self.figure._get_renderer()
+
+ if not self.get_visible():
+ self._axislabel_pad = self._external_pad
+ return []
+
+ bboxes = []
+
+ r, total_width = self._get_ticklabels_offsets(renderer,
+ self._axis_direction)
+
+ pad = self._external_pad + renderer.points_to_pixels(self.get_pad())
+ self._offset_radius = r + pad
+
+ for (x, y), a, l in self._locs_angles_labels:
+ self._ref_angle = a
+ self.set_x(x)
+ self.set_y(y)
+ self.set_text(l)
+ bb = LabelBase.get_window_extent(self, renderer)
+ bboxes.append(bb)
+
+ # the value saved will be used to draw axislabel.
+ self._axislabel_pad = total_width + pad
+
+ return bboxes
+
+ def get_texts_widths_heights_descents(self, renderer):
+ """
+ Return a list of ``(width, height, descent)`` tuples for ticklabels.
+
+ Empty labels are left out.
+ """
+ whd_list = []
+ for _loc, _angle, label in self._locs_angles_labels:
+ if not label.strip():
+ continue
+ clean_line, ismath = self._preprocess_math(label)
+ whd = renderer.get_text_width_height_descent(
+ clean_line, self._fontproperties, ismath=ismath)
+ whd_list.append(whd)
+ return whd_list
+
+
+class GridlinesCollection(LineCollection):
+ def __init__(self, *args, which="major", axis="both", **kwargs):
+ """
+ Collection of grid lines.
+
+ Parameters
+ ----------
+ which : {"major", "minor"}
+ Which grid to consider.
+ axis : {"both", "x", "y"}
+ Which axis to consider.
+ *args, **kwargs
+ Passed to `.LineCollection`.
+ """
+ self._which = which
+ self._axis = axis
+ super().__init__(*args, **kwargs)
+ self.set_grid_helper(None)
+
+ def set_which(self, which):
+ """
+ Select major or minor grid lines.
+
+ Parameters
+ ----------
+ which : {"major", "minor"}
+ """
+ self._which = which
+
+ def set_axis(self, axis):
+ """
+ Select axis.
+
+ Parameters
+ ----------
+ axis : {"both", "x", "y"}
+ """
+ self._axis = axis
+
+ def set_grid_helper(self, grid_helper):
+ """
+ Set grid helper.
+
+ Parameters
+ ----------
+ grid_helper : `.GridHelperBase` subclass
+ """
+ self._grid_helper = grid_helper
+
+ def draw(self, renderer):
+ if self._grid_helper is not None:
+ self._grid_helper.update_lim(self.axes)
+ gl = self._grid_helper.get_gridlines(self._which, self._axis)
+ self.set_segments([np.transpose(l) for l in gl])
+ super().draw(renderer)
+
+
+class AxisArtist(martist.Artist):
+ """
+ An artist which draws axis (a line along which the n-th axes coord
+ is constant) line, ticks, tick labels, and axis label.
+ """
+
+ zorder = 2.5
+
+ @property
+ def LABELPAD(self):
+ return self.label.get_pad()
+
+ @LABELPAD.setter
+ def LABELPAD(self, v):
+ self.label.set_pad(v)
+
+ def __init__(self, axes,
+ helper,
+ offset=None,
+ axis_direction="bottom",
+ **kwargs):
+ """
+ Parameters
+ ----------
+ axes : `mpl_toolkits.axisartist.axislines.Axes`
+ helper : `~mpl_toolkits.axisartist.axislines.AxisArtistHelper`
+ """
+ # axes is also used to follow the axis attribute (tick color, etc).
+
+ super().__init__(**kwargs)
+
+ self.axes = axes
+
+ self._axis_artist_helper = helper
+
+ if offset is None:
+ offset = (0, 0)
+ self.offset_transform = ScaledTranslation(
+ *offset,
+ Affine2D().scale(1 / 72) # points to inches.
+ + self.axes.figure.dpi_scale_trans)
+
+ if axis_direction in ["left", "right"]:
+ self.axis = axes.yaxis
+ else:
+ self.axis = axes.xaxis
+
+ self._axisline_style = None
+ self._axis_direction = axis_direction
+
+ self._init_line()
+ self._init_ticks(**kwargs)
+ self._init_offsetText(axis_direction)
+ self._init_label()
+
+ # axis direction
+ self._ticklabel_add_angle = 0.
+ self._axislabel_add_angle = 0.
+ self.set_axis_direction(axis_direction)
+
+ # axis direction
+
+ def set_axis_direction(self, axis_direction):
+ """
+ Adjust the direction, text angle, and text alignment of tick labels
+ and axis labels following the Matplotlib convention for the rectangle
+ axes.
+
+ The *axis_direction* must be one of [left, right, bottom, top].
+
+ ===================== ========== ========= ========== ==========
+ Property left bottom right top
+ ===================== ========== ========= ========== ==========
+ ticklabel direction "-" "+" "+" "-"
+ axislabel direction "-" "+" "+" "-"
+ ticklabel angle 90 0 -90 180
+ ticklabel va center baseline center baseline
+ ticklabel ha right center right center
+ axislabel angle 180 0 0 180
+ axislabel va center top center bottom
+ axislabel ha right center right center
+ ===================== ========== ========= ========== ==========
+
+ Note that the direction "+" and "-" are relative to the direction of
+ the increasing coordinate. Also, the text angles are actually
+ relative to (90 + angle of the direction to the ticklabel),
+ which gives 0 for bottom axis.
+
+ Parameters
+ ----------
+ axis_direction : {"left", "bottom", "right", "top"}
+ """
+ self.major_ticklabels.set_axis_direction(axis_direction)
+ self.label.set_axis_direction(axis_direction)
+ self._axis_direction = axis_direction
+ if axis_direction in ["left", "top"]:
+ self.set_ticklabel_direction("-")
+ self.set_axislabel_direction("-")
+ else:
+ self.set_ticklabel_direction("+")
+ self.set_axislabel_direction("+")
+
+ def set_ticklabel_direction(self, tick_direction):
+ r"""
+ Adjust the direction of the tick labels.
+
+ Note that the *tick_direction*\s '+' and '-' are relative to the
+ direction of the increasing coordinate.
+
+ Parameters
+ ----------
+ tick_direction : {"+", "-"}
+ """
+ self._ticklabel_add_angle = _api.check_getitem(
+ {"+": 0, "-": 180}, tick_direction=tick_direction)
+
+ def invert_ticklabel_direction(self):
+ self._ticklabel_add_angle = (self._ticklabel_add_angle + 180) % 360
+ self.major_ticklabels.invert_axis_direction()
+ self.minor_ticklabels.invert_axis_direction()
+
+ def set_axislabel_direction(self, label_direction):
+ r"""
+ Adjust the direction of the axis label.
+
+ Note that the *label_direction*\s '+' and '-' are relative to the
+ direction of the increasing coordinate.
+
+ Parameters
+ ----------
+ label_direction : {"+", "-"}
+ """
+ self._axislabel_add_angle = _api.check_getitem(
+ {"+": 0, "-": 180}, label_direction=label_direction)
+
+ def get_transform(self):
+ return self.axes.transAxes + self.offset_transform
+
+ def get_helper(self):
+ """
+ Return axis artist helper instance.
+ """
+ return self._axis_artist_helper
+
+ def set_axisline_style(self, axisline_style=None, **kwargs):
+ """
+ Set the axisline style.
+
+ The new style is completely defined by the passed attributes. Existing
+ style attributes are forgotten.
+
+ Parameters
+ ----------
+ axisline_style : str or None
+ The line style, e.g. '->', optionally followed by a comma-separated
+ list of attributes. Alternatively, the attributes can be provided
+ as keywords.
+
+ If *None* this returns a string containing the available styles.
+
+ Examples
+ --------
+ The following two commands are equal:
+
+ >>> set_axisline_style("->,size=1.5")
+ >>> set_axisline_style("->", size=1.5)
+ """
+ if axisline_style is None:
+ return AxislineStyle.pprint_styles()
+
+ if isinstance(axisline_style, AxislineStyle._Base):
+ self._axisline_style = axisline_style
+ else:
+ self._axisline_style = AxislineStyle(axisline_style, **kwargs)
+
+ self._init_line()
+
+ def get_axisline_style(self):
+ """Return the current axisline style."""
+ return self._axisline_style
+
+ def _init_line(self):
+ """
+ Initialize the *line* artist that is responsible to draw the axis line.
+ """
+ tran = (self._axis_artist_helper.get_line_transform(self.axes)
+ + self.offset_transform)
+
+ axisline_style = self.get_axisline_style()
+ if axisline_style is None:
+ self.line = PathPatch(
+ self._axis_artist_helper.get_line(self.axes),
+ color=mpl.rcParams['axes.edgecolor'],
+ fill=False,
+ linewidth=mpl.rcParams['axes.linewidth'],
+ capstyle=mpl.rcParams['lines.solid_capstyle'],
+ joinstyle=mpl.rcParams['lines.solid_joinstyle'],
+ transform=tran)
+ else:
+ self.line = axisline_style(self, transform=tran)
+
+ def _draw_line(self, renderer):
+ self.line.set_path(self._axis_artist_helper.get_line(self.axes))
+ if self.get_axisline_style() is not None:
+ self.line.set_line_mutation_scale(self.major_ticklabels.get_size())
+ self.line.draw(renderer)
+
+ def _init_ticks(self, **kwargs):
+ axis_name = self.axis.axis_name
+
+ trans = (self._axis_artist_helper.get_tick_transform(self.axes)
+ + self.offset_transform)
+
+ self.major_ticks = Ticks(
+ kwargs.get(
+ "major_tick_size",
+ mpl.rcParams[f"{axis_name}tick.major.size"]),
+ axis=self.axis, transform=trans)
+ self.minor_ticks = Ticks(
+ kwargs.get(
+ "minor_tick_size",
+ mpl.rcParams[f"{axis_name}tick.minor.size"]),
+ axis=self.axis, transform=trans)
+
+ size = mpl.rcParams[f"{axis_name}tick.labelsize"]
+ self.major_ticklabels = TickLabels(
+ axis=self.axis,
+ axis_direction=self._axis_direction,
+ figure=self.axes.figure,
+ transform=trans,
+ fontsize=size,
+ pad=kwargs.get(
+ "major_tick_pad", mpl.rcParams[f"{axis_name}tick.major.pad"]),
+ )
+ self.minor_ticklabels = TickLabels(
+ axis=self.axis,
+ axis_direction=self._axis_direction,
+ figure=self.axes.figure,
+ transform=trans,
+ fontsize=size,
+ pad=kwargs.get(
+ "minor_tick_pad", mpl.rcParams[f"{axis_name}tick.minor.pad"]),
+ )
+
+ def _get_tick_info(self, tick_iter):
+ """
+ Return a pair of:
+
+ - list of locs and angles for ticks
+ - list of locs, angles and labels for ticklabels.
+ """
+ ticks_loc_angle = []
+ ticklabels_loc_angle_label = []
+
+ ticklabel_add_angle = self._ticklabel_add_angle
+
+ for loc, angle_normal, angle_tangent, label in tick_iter:
+ angle_label = angle_tangent - 90 + ticklabel_add_angle
+ angle_tick = (angle_normal
+ if 90 <= (angle_label - angle_normal) % 360 <= 270
+ else angle_normal + 180)
+ ticks_loc_angle.append([loc, angle_tick])
+ ticklabels_loc_angle_label.append([loc, angle_label, label])
+
+ return ticks_loc_angle, ticklabels_loc_angle_label
+
+ def _update_ticks(self, renderer=None):
+ # set extra pad for major and minor ticklabels: use ticksize of
+ # majorticks even for minor ticks. not clear what is best.
+
+ if renderer is None:
+ renderer = self.figure._get_renderer()
+
+ dpi_cor = renderer.points_to_pixels(1.)
+ if self.major_ticks.get_visible() and self.major_ticks.get_tick_out():
+ ticklabel_pad = self.major_ticks._ticksize * dpi_cor
+ self.major_ticklabels._external_pad = ticklabel_pad
+ self.minor_ticklabels._external_pad = ticklabel_pad
+ else:
+ self.major_ticklabels._external_pad = 0
+ self.minor_ticklabels._external_pad = 0
+
+ majortick_iter, minortick_iter = \
+ self._axis_artist_helper.get_tick_iterators(self.axes)
+
+ tick_loc_angle, ticklabel_loc_angle_label = \
+ self._get_tick_info(majortick_iter)
+ self.major_ticks.set_locs_angles(tick_loc_angle)
+ self.major_ticklabels.set_locs_angles_labels(ticklabel_loc_angle_label)
+
+ tick_loc_angle, ticklabel_loc_angle_label = \
+ self._get_tick_info(minortick_iter)
+ self.minor_ticks.set_locs_angles(tick_loc_angle)
+ self.minor_ticklabels.set_locs_angles_labels(ticklabel_loc_angle_label)
+
+ def _draw_ticks(self, renderer):
+ self._update_ticks(renderer)
+ self.major_ticks.draw(renderer)
+ self.major_ticklabels.draw(renderer)
+ self.minor_ticks.draw(renderer)
+ self.minor_ticklabels.draw(renderer)
+ if (self.major_ticklabels.get_visible()
+ or self.minor_ticklabels.get_visible()):
+ self._draw_offsetText(renderer)
+
+ _offsetText_pos = dict(left=(0, 1, "bottom", "right"),
+ right=(1, 1, "bottom", "left"),
+ bottom=(1, 0, "top", "right"),
+ top=(1, 1, "bottom", "right"))
+
+ def _init_offsetText(self, direction):
+ x, y, va, ha = self._offsetText_pos[direction]
+ self.offsetText = mtext.Annotation(
+ "",
+ xy=(x, y), xycoords="axes fraction",
+ xytext=(0, 0), textcoords="offset points",
+ color=mpl.rcParams['xtick.color'],
+ horizontalalignment=ha, verticalalignment=va,
+ )
+ self.offsetText.set_transform(IdentityTransform())
+ self.axes._set_artist_props(self.offsetText)
+
+ def _update_offsetText(self):
+ self.offsetText.set_text(self.axis.major.formatter.get_offset())
+ self.offsetText.set_size(self.major_ticklabels.get_size())
+ offset = (self.major_ticklabels.get_pad()
+ + self.major_ticklabels.get_size()
+ + 2)
+ self.offsetText.xyann = (0, offset)
+
+ def _draw_offsetText(self, renderer):
+ self._update_offsetText()
+ self.offsetText.draw(renderer)
+
+ def _init_label(self, **kwargs):
+ tr = (self._axis_artist_helper.get_axislabel_transform(self.axes)
+ + self.offset_transform)
+ self.label = AxisLabel(
+ 0, 0, "__from_axes__",
+ color="auto",
+ fontsize=kwargs.get("labelsize", mpl.rcParams['axes.labelsize']),
+ fontweight=mpl.rcParams['axes.labelweight'],
+ axis=self.axis,
+ transform=tr,
+ axis_direction=self._axis_direction,
+ )
+ self.label.set_figure(self.axes.figure)
+ labelpad = kwargs.get("labelpad", 5)
+ self.label.set_pad(labelpad)
+
+ def _update_label(self, renderer):
+ if not self.label.get_visible():
+ return
+
+ if self._ticklabel_add_angle != self._axislabel_add_angle:
+ if ((self.major_ticks.get_visible()
+ and not self.major_ticks.get_tick_out())
+ or (self.minor_ticks.get_visible()
+ and not self.major_ticks.get_tick_out())):
+ axislabel_pad = self.major_ticks._ticksize
+ else:
+ axislabel_pad = 0
+ else:
+ axislabel_pad = max(self.major_ticklabels._axislabel_pad,
+ self.minor_ticklabels._axislabel_pad)
+
+ self.label._external_pad = axislabel_pad
+
+ xy, angle_tangent = \
+ self._axis_artist_helper.get_axislabel_pos_angle(self.axes)
+ if xy is None:
+ return
+
+ angle_label = angle_tangent - 90
+
+ x, y = xy
+ self.label._ref_angle = angle_label + self._axislabel_add_angle
+ self.label.set(x=x, y=y)
+
+ def _draw_label(self, renderer):
+ self._update_label(renderer)
+ self.label.draw(renderer)
+
+ def set_label(self, s):
+ # docstring inherited
+ self.label.set_text(s)
+
+ def get_tightbbox(self, renderer=None):
+ if not self.get_visible():
+ return
+ self._axis_artist_helper.update_lim(self.axes)
+ self._update_ticks(renderer)
+ self._update_label(renderer)
+
+ self.line.set_path(self._axis_artist_helper.get_line(self.axes))
+ if self.get_axisline_style() is not None:
+ self.line.set_line_mutation_scale(self.major_ticklabels.get_size())
+
+ bb = [
+ *self.major_ticklabels.get_window_extents(renderer),
+ *self.minor_ticklabels.get_window_extents(renderer),
+ self.label.get_window_extent(renderer),
+ self.offsetText.get_window_extent(renderer),
+ self.line.get_window_extent(renderer),
+ ]
+ bb = [b for b in bb if b and (b.width != 0 or b.height != 0)]
+ if bb:
+ _bbox = Bbox.union(bb)
+ return _bbox
+ else:
+ return None
+
+ @martist.allow_rasterization
+ def draw(self, renderer):
+ # docstring inherited
+ if not self.get_visible():
+ return
+ renderer.open_group(__name__, gid=self.get_gid())
+ self._axis_artist_helper.update_lim(self.axes)
+ self._draw_ticks(renderer)
+ self._draw_line(renderer)
+ self._draw_label(renderer)
+ renderer.close_group(__name__)
+
+ def toggle(self, all=None, ticks=None, ticklabels=None, label=None):
+ """
+ Toggle visibility of ticks, ticklabels, and (axis) label.
+ To turn all off, ::
+
+ axis.toggle(all=False)
+
+ To turn all off but ticks on ::
+
+ axis.toggle(all=False, ticks=True)
+
+ To turn all on but (axis) label off ::
+
+ axis.toggle(all=True, label=False)
+
+ """
+ if all:
+ _ticks, _ticklabels, _label = True, True, True
+ elif all is not None:
+ _ticks, _ticklabels, _label = False, False, False
+ else:
+ _ticks, _ticklabels, _label = None, None, None
+
+ if ticks is not None:
+ _ticks = ticks
+ if ticklabels is not None:
+ _ticklabels = ticklabels
+ if label is not None:
+ _label = label
+
+ if _ticks is not None:
+ self.major_ticks.set_visible(_ticks)
+ self.minor_ticks.set_visible(_ticks)
+ if _ticklabels is not None:
+ self.major_ticklabels.set_visible(_ticklabels)
+ self.minor_ticklabels.set_visible(_ticklabels)
+ if _label is not None:
+ self.label.set_visible(_label)
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axisline_style.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axisline_style.py
new file mode 100644
index 0000000000..5ae188021b
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axisline_style.py
@@ -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 : 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
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axislines.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axislines.py
new file mode 100644
index 0000000000..35717da8ea
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axislines.py
@@ -0,0 +1,531 @@
+"""
+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 update_lim(self, axes):
+ pass
+
+ 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."""
+
+ passthru_pt = _api.deprecated("3.7")(property(
+ lambda self: {"left": (0, 0), "right": (1, 0),
+ "bottom": (0, 0), "top": (0, 1)}[self._loc]))
+
+ def __init__(self, loc, nth_coord=None):
+ """``nth_coord = 0``: x-axis; ``nth_coord = 1``: y-axis."""
+ self.nth_coord = (
+ nth_coord if nth_coord is not None else
+ _api.check_getitem(
+ {"bottom": 0, "top": 0, "left": 1, "right": 1}, loc=loc))
+ if (nth_coord == 0 and loc not in ["left", "right"]
+ or nth_coord == 1 and loc not in ["bottom", "top"]):
+ _api.warn_deprecated(
+ "3.7", message=f"{loc=!r} is incompatible with "
+ "{nth_coord=}; support is deprecated since %(since)s")
+ self._loc = loc
+ self._pos = {"bottom": 0, "top": 1, "left": 0, "right": 1}[loc]
+ super().__init__()
+ # axis line in transAxes
+ self._path = Path(self._to_xy((0, 1), const=self._pos))
+
+ def get_nth_coord(self):
+ return self.nth_coord
+
+ # LINE
+
+ def get_line(self, axes):
+ return self._path
+
+ def get_line_transform(self, axes):
+ return axes.transAxes
+
+ # LABEL
+
+ def get_axislabel_transform(self, axes):
+ return axes.transAxes
+
+ def get_axislabel_pos_angle(self, axes):
+ """
+ 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.nth_coord = nth_coord
+ self._value = value
+ super().__init__()
+
+ def get_nth_coord(self):
+ return self.nth_coord
+
+ def get_line(self, axes):
+ raise RuntimeError(
+ "get_line method should be defined by the derived class")
+
+
+class FixedAxisArtistHelperRectilinear(_FixedAxisArtistHelperBase):
+
+ def __init__(self, axes, loc, nth_coord=None):
+ """
+ nth_coord = along which coordinate value varies
+ in 2D, nth_coord = 0 -> x axis, nth_coord = 1 -> y axis
+ """
+ super().__init__(loc, nth_coord)
+ self.axis = [axes.xaxis, axes.yaxis][self.nth_coord]
+
+ # TICK
+
+ def get_tick_iterators(self, axes):
+ """tick_loc, tick_angle, tick_label"""
+ if self._loc in ["bottom", "top"]:
+ angle_normal, angle_tangent = 90, 0
+ else: # "left", "right"
+ angle_normal, angle_tangent = 0, 90
+
+ 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])
+ if 0 <= verts[fixed_coord] <= 1:
+ return verts, angle
+ else:
+ return None, None
+
+ def get_tick_transform(self, axes):
+ return axes.transData
+
+ def get_tick_iterators(self, axes):
+ """tick_loc, tick_angle, tick_label"""
+ if self.nth_coord == 0:
+ angle_normal, angle_tangent = 90, 0
+ else:
+ angle_normal, angle_tangent = 0, 90
+
+ 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
+
+ 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
+
+ helper = FixedAxisArtistHelperRectilinear(axes, loc, nth_coord)
+ axisline = AxisArtist(axes, helper, offset=offset,
+ axis_direction=axis_direction)
+ return axisline
+
+ def new_floating_axis(self, nth_coord, value,
+ axis_direction="bottom",
+ axes=None,
+ ):
+ if axes is None:
+ _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())
+
+ for x in locs:
+ gridlines.append([[x, x], [y1, y2]])
+
+ if axis in ("both", "y"):
+ x1, x2 = self.axes.get_xlim()
+ locs = []
+ if self.axes.yaxis._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())
+
+ for y in locs:
+ gridlines.append([[x1, x2], [y, y]])
+
+ return gridlines
+
+
+class Axes(maxes.Axes):
+
+ @_api.deprecated("3.8", alternative="ax.axis")
+ def __call__(self, *args, **kwargs):
+ return maxes.Axes.axis(self.axes, *args, **kwargs)
+
+ def __init__(self, *args, grid_helper=None, **kwargs):
+ self._axisline_on = True
+ self._grid_helper = (grid_helper if grid_helper
+ else GridHelperRectlinear(self))
+ super().__init__(*args, **kwargs)
+ self.toggle_axisline(True)
+
+ def toggle_axisline(self, b=None):
+ if b is None:
+ b = not self._axisline_on
+ if b:
+ self._axisline_on = True
+ self.spines[:].set_visible(False)
+ self.xaxis.set_visible(False)
+ self.yaxis.set_visible(False)
+ else:
+ self._axisline_on = False
+ self.spines[:].set_visible(True)
+ self.xaxis.set_visible(True)
+ self.yaxis.set_visible(True)
+
+ @property
+ def axis(self):
+ return self._axislines
+
+ def clear(self):
+ # docstring inherited
+
+ # Init gridlines before clear() as clear() calls grid().
+ self.gridlines = gridlines = GridlinesCollection(
+ [],
+ colors=mpl.rcParams['grid.color'],
+ linestyles=mpl.rcParams['grid.linestyle'],
+ linewidths=mpl.rcParams['grid.linewidth'])
+ self._set_artist_props(gridlines)
+ gridlines.set_grid_helper(self.get_grid_helper())
+
+ super().clear()
+
+ # clip_path is set after Axes.clear(): that's when a patch is created.
+ gridlines.set_clip_path(self.axes.patch)
+
+ # Init axis artists.
+ self._axislines = mpl_axes.Axes.AxisDict(self)
+ new_fixed_axis = self.get_grid_helper().new_fixed_axis
+ self._axislines.update({
+ loc: new_fixed_axis(loc=loc, axes=self, axis_direction=loc)
+ for loc in ["bottom", "top", "left", "right"]})
+ for axisline in [self._axislines["top"], self._axislines["right"]]:
+ axisline.label.set_visible(False)
+ axisline.major_ticklabels.set_visible(False)
+ axisline.minor_ticklabels.set_visible(False)
+
+ def get_grid_helper(self):
+ return self._grid_helper
+
+ def grid(self, visible=None, which='major', axis="both", **kwargs):
+ """
+ Toggle the gridlines, and optionally set the properties of the lines.
+ """
+ # There are some discrepancies in the behavior of grid() between
+ # axes_grid and Matplotlib, because axes_grid explicitly sets the
+ # visibility of the gridlines.
+ super().grid(visible, which=which, axis=axis, **kwargs)
+ if not self._axisline_on:
+ return
+ if visible is None:
+ visible = (self.axes.xaxis._minor_tick_kw["gridOn"]
+ or self.axes.xaxis._major_tick_kw["gridOn"]
+ or self.axes.yaxis._minor_tick_kw["gridOn"]
+ or self.axes.yaxis._major_tick_kw["gridOn"])
+ self.gridlines.set(which=which, axis=axis, visible=visible)
+ self.gridlines.set(**kwargs)
+
+ def get_children(self):
+ if self._axisline_on:
+ children = [*self._axislines.values(), self.gridlines]
+ else:
+ children = []
+ children.extend(super().get_children())
+ return children
+
+ def new_fixed_axis(self, loc, offset=None):
+ gh = self.get_grid_helper()
+ axis = gh.new_fixed_axis(loc,
+ nth_coord=None,
+ axis_direction=None,
+ offset=offset,
+ axes=self,
+ )
+ return axis
+
+ def new_floating_axis(self, nth_coord, value, axis_direction="bottom"):
+ gh = self.get_grid_helper()
+ axis = gh.new_floating_axis(nth_coord, value,
+ axis_direction=axis_direction,
+ axes=self)
+ return axis
+
+
+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
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/floating_axes.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/floating_axes.py
new file mode 100644
index 0000000000..97dafe98c6
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/floating_axes.py
@@ -0,0 +1,298 @@
+"""
+An experimental support for curvilinear grid.
+"""
+
+# TODO :
+# see if tick_iterator method can be simplified by reusing the parent method.
+
+import functools
+
+import numpy as np
+
+import matplotlib as mpl
+from matplotlib import _api, cbook
+import matplotlib.patches as mpatches
+from matplotlib.path import Path
+
+from mpl_toolkits.axes_grid1.parasite_axes import host_axes_class_factory
+
+from . import axislines, grid_helper_curvelinear
+from .axis_artist import AxisArtist
+from .grid_finder import ExtremeFinderSimple
+
+
+class FloatingAxisArtistHelper(
+ grid_helper_curvelinear.FloatingAxisArtistHelper):
+ pass
+
+
+class FixedAxisArtistHelper(grid_helper_curvelinear.FloatingAxisArtistHelper):
+
+ def __init__(self, grid_helper, side, nth_coord_ticks=None):
+ """
+ nth_coord = along which coordinate value varies.
+ nth_coord = 0 -> x axis, nth_coord = 1 -> y axis
+ """
+ lon1, lon2, lat1, lat2 = grid_helper.grid_finder.extreme_finder(*[None] * 5)
+ value, nth_coord = _api.check_getitem(
+ dict(left=(lon1, 0), right=(lon2, 0), bottom=(lat1, 1), top=(lat2, 1)),
+ side=side)
+ super().__init__(grid_helper, nth_coord, value, axis_direction=side)
+ if nth_coord_ticks is None:
+ nth_coord_ticks = nth_coord
+ self.nth_coord_ticks = nth_coord_ticks
+
+ self.value = value
+ self.grid_helper = grid_helper
+ self._side = side
+
+ def update_lim(self, axes):
+ self.grid_helper.update_lim(axes)
+ self._grid_info = self.grid_helper._grid_info
+
+ def get_tick_iterators(self, axes):
+ """tick_loc, tick_angle, tick_label, (optionally) tick_label"""
+
+ grid_finder = self.grid_helper.grid_finder
+
+ lat_levs, lat_n, lat_factor = self._grid_info["lat_info"]
+ yy0 = lat_levs / lat_factor
+
+ lon_levs, lon_n, lon_factor = self._grid_info["lon_info"]
+ xx0 = lon_levs / lon_factor
+
+ extremes = self.grid_helper.grid_finder.extreme_finder(*[None] * 5)
+ xmin, xmax = sorted(extremes[:2])
+ ymin, ymax = sorted(extremes[2:])
+
+ def trf_xy(x, y):
+ trf = grid_finder.get_transform() + axes.transData
+ return trf.transform(np.column_stack(np.broadcast_arrays(x, y))).T
+
+ if self.nth_coord == 0:
+ mask = (ymin <= yy0) & (yy0 <= ymax)
+ (xx1, yy1), (dxx1, dyy1), (dxx2, dyy2) = \
+ grid_helper_curvelinear._value_and_jacobian(
+ trf_xy, self.value, yy0[mask], (xmin, xmax), (ymin, ymax))
+ labels = self._grid_info["lat_labels"]
+
+ elif self.nth_coord == 1:
+ mask = (xmin <= xx0) & (xx0 <= xmax)
+ (xx1, yy1), (dxx2, dyy2), (dxx1, dyy1) = \
+ grid_helper_curvelinear._value_and_jacobian(
+ trf_xy, xx0[mask], self.value, (xmin, xmax), (ymin, ymax))
+ labels = self._grid_info["lon_labels"]
+
+ labels = [l for l, m in zip(labels, mask) if m]
+
+ angle_normal = np.arctan2(dyy1, dxx1)
+ angle_tangent = np.arctan2(dyy2, dxx2)
+ mm = (dyy1 == 0) & (dxx1 == 0) # points with degenerate normal
+ angle_normal[mm] = angle_tangent[mm] + np.pi / 2
+
+ tick_to_axes = self.get_tick_transform(axes) - axes.transAxes
+ in_01 = functools.partial(
+ mpl.transforms._interval_contains_close, (0, 1))
+
+ def f1():
+ for x, y, normal, tangent, lab \
+ in zip(xx1, yy1, angle_normal, angle_tangent, labels):
+ c2 = tick_to_axes.transform((x, y))
+ if in_01(c2[0]) and in_01(c2[1]):
+ yield [x, y], *np.rad2deg([normal, tangent]), lab
+
+ return f1(), iter([])
+
+ def get_line(self, axes):
+ self.update_lim(axes)
+ k, v = dict(left=("lon_lines0", 0),
+ right=("lon_lines0", 1),
+ bottom=("lat_lines0", 0),
+ top=("lat_lines0", 1))[self._side]
+ xx, yy = self._grid_info[k][v]
+ return Path(np.column_stack([xx, yy]))
+
+
+class ExtremeFinderFixed(ExtremeFinderSimple):
+ # docstring inherited
+
+ def __init__(self, extremes):
+ """
+ This subclass always returns the same bounding box.
+
+ Parameters
+ ----------
+ extremes : (float, float, float, float)
+ The bounding box that this helper always returns.
+ """
+ self._extremes = extremes
+
+ def __call__(self, transform_xy, x1, y1, x2, y2):
+ # docstring inherited
+ return self._extremes
+
+
+class GridHelperCurveLinear(grid_helper_curvelinear.GridHelperCurveLinear):
+
+ def __init__(self, aux_trans, extremes,
+ grid_locator1=None,
+ grid_locator2=None,
+ tick_formatter1=None,
+ tick_formatter2=None):
+ # docstring inherited
+ super().__init__(aux_trans,
+ extreme_finder=ExtremeFinderFixed(extremes),
+ grid_locator1=grid_locator1,
+ grid_locator2=grid_locator2,
+ tick_formatter1=tick_formatter1,
+ tick_formatter2=tick_formatter2)
+
+ @_api.deprecated("3.8")
+ def get_data_boundary(self, side):
+ """
+ Return v=0, nth=1.
+ """
+ lon1, lon2, lat1, lat2 = self.grid_finder.extreme_finder(*[None] * 5)
+ return dict(left=(lon1, 0),
+ right=(lon2, 0),
+ bottom=(lat1, 1),
+ top=(lat2, 1))[side]
+
+ def new_fixed_axis(self, loc,
+ nth_coord=None,
+ axis_direction=None,
+ offset=None,
+ axes=None):
+ if axes is None:
+ axes = self.axes
+ if axis_direction is None:
+ axis_direction = loc
+ # This is not the same as the FixedAxisArtistHelper class used by
+ # grid_helper_curvelinear.GridHelperCurveLinear.new_fixed_axis!
+ helper = FixedAxisArtistHelper(
+ self, loc, nth_coord_ticks=nth_coord)
+ axisline = AxisArtist(axes, helper, axis_direction=axis_direction)
+ # Perhaps should be moved to the base class?
+ axisline.line.set_clip_on(True)
+ axisline.line.set_clip_box(axisline.axes.bbox)
+ return axisline
+
+ # new_floating_axis will inherit the grid_helper's extremes.
+
+ # def new_floating_axis(self, nth_coord,
+ # value,
+ # axes=None,
+ # axis_direction="bottom"
+ # ):
+
+ # axis = super(GridHelperCurveLinear,
+ # self).new_floating_axis(nth_coord,
+ # value, axes=axes,
+ # axis_direction=axis_direction)
+
+ # # set extreme values of the axis helper
+ # if nth_coord == 1:
+ # axis.get_helper().set_extremes(*self._extremes[:2])
+ # elif nth_coord == 0:
+ # axis.get_helper().set_extremes(*self._extremes[2:])
+
+ # return axis
+
+ def _update_grid(self, x1, y1, x2, y2):
+ if self._grid_info is None:
+ self._grid_info = dict()
+
+ grid_info = self._grid_info
+
+ grid_finder = self.grid_finder
+ extremes = grid_finder.extreme_finder(grid_finder.inv_transform_xy,
+ x1, y1, x2, y2)
+
+ lon_min, lon_max = sorted(extremes[:2])
+ lat_min, lat_max = sorted(extremes[2:])
+ grid_info["extremes"] = lon_min, lon_max, lat_min, lat_max # extremes
+
+ lon_levs, lon_n, lon_factor = \
+ grid_finder.grid_locator1(lon_min, lon_max)
+ lon_levs = np.asarray(lon_levs)
+ lat_levs, lat_n, lat_factor = \
+ grid_finder.grid_locator2(lat_min, lat_max)
+ lat_levs = np.asarray(lat_levs)
+
+ grid_info["lon_info"] = lon_levs, lon_n, lon_factor
+ grid_info["lat_info"] = lat_levs, lat_n, lat_factor
+
+ grid_info["lon_labels"] = grid_finder.tick_formatter1(
+ "bottom", lon_factor, lon_levs)
+ grid_info["lat_labels"] = grid_finder.tick_formatter2(
+ "bottom", lat_factor, lat_levs)
+
+ lon_values = lon_levs[:lon_n] / lon_factor
+ lat_values = lat_levs[:lat_n] / lat_factor
+
+ lon_lines, lat_lines = grid_finder._get_raw_grid_lines(
+ lon_values[(lon_min < lon_values) & (lon_values < lon_max)],
+ lat_values[(lat_min < lat_values) & (lat_values < lat_max)],
+ lon_min, lon_max, lat_min, lat_max)
+
+ grid_info["lon_lines"] = lon_lines
+ grid_info["lat_lines"] = lat_lines
+
+ lon_lines, lat_lines = grid_finder._get_raw_grid_lines(
+ # lon_min, lon_max, lat_min, lat_max)
+ extremes[:2], extremes[2:], *extremes)
+
+ grid_info["lon_lines0"] = lon_lines
+ grid_info["lat_lines0"] = lat_lines
+
+ def get_gridlines(self, which="major", axis="both"):
+ grid_lines = []
+ if axis in ["both", "x"]:
+ grid_lines.extend(self._grid_info["lon_lines"])
+ if axis in ["both", "y"]:
+ grid_lines.extend(self._grid_info["lat_lines"])
+ return grid_lines
+
+
+class FloatingAxesBase:
+
+ def __init__(self, *args, grid_helper, **kwargs):
+ _api.check_isinstance(GridHelperCurveLinear, grid_helper=grid_helper)
+ super().__init__(*args, grid_helper=grid_helper, **kwargs)
+ self.set_aspect(1.)
+
+ def _gen_axes_patch(self):
+ # docstring inherited
+ x0, x1, y0, y1 = self.get_grid_helper().grid_finder.extreme_finder(*[None] * 5)
+ patch = mpatches.Polygon([(x0, y0), (x1, y0), (x1, y1), (x0, y1)])
+ patch.get_path()._interpolation_steps = 100
+ return patch
+
+ def clear(self):
+ super().clear()
+ self.patch.set_transform(
+ self.get_grid_helper().grid_finder.get_transform()
+ + self.transData)
+ # The original patch is not in the draw tree; it is only used for
+ # clipping purposes.
+ orig_patch = super()._gen_axes_patch()
+ orig_patch.set_figure(self.figure)
+ orig_patch.set_transform(self.transAxes)
+ self.patch.set_clip_path(orig_patch)
+ self.gridlines.set_clip_path(orig_patch)
+ self.adjust_axes_lim()
+
+ def adjust_axes_lim(self):
+ bbox = self.patch.get_path().get_extents(
+ # First transform to pixel coords, then to parent data coords.
+ self.patch.get_transform() - self.transData)
+ bbox = bbox.expanded(1.02, 1.02)
+ self.set_xlim(bbox.xmin, bbox.xmax)
+ self.set_ylim(bbox.ymin, bbox.ymax)
+
+
+floatingaxes_class_factory = cbook._make_class_factory(
+ FloatingAxesBase, "Floating{}")
+FloatingAxes = floatingaxes_class_factory(
+ host_axes_class_factory(axislines.Axes))
+FloatingSubplot = FloatingAxes
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/grid_finder.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/grid_finder.py
new file mode 100644
index 0000000000..f969b011c4
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/grid_finder.py
@@ -0,0 +1,335 @@
+import numpy as np
+
+from matplotlib import ticker as mticker
+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)]:
+ crossings.append([])
+ 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]))
+ crossings[-1].append((crossing, theta))
+ 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 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)
+
+ ddx = (x2-x1)*1.e-10
+ ddy = (y2-y1)*1.e-10
+ bb = Bbox.from_extents(x1-ddx, y1-ddy, x2+ddx, y2+ddy)
+
+ grid_info = {
+ "extremes": extremes,
+ "lon_lines": lon_lines,
+ "lat_lines": lat_lines,
+ "lon": self._clip_grid_lines_and_find_ticks(
+ lon_lines, lon_values, lon_levs, bb),
+ "lat": self._clip_grid_lines_and_find_ticks(
+ lat_lines, lat_values, lat_levs, bb),
+ }
+
+ tck_labels = grid_info["lon"]["tick_labels"] = {}
+ for direction in ["left", "bottom", "right", "top"]:
+ levs = grid_info["lon"]["tick_levels"][direction]
+ tck_labels[direction] = self.tick_formatter1(
+ direction, lon_factor, levs)
+
+ tck_labels = grid_info["lat"]["tick_labels"] = {}
+ for direction in ["left", "bottom", "right", "top"]:
+ levs = grid_info["lat"]["tick_levels"][direction]
+ tck_labels[direction] = self.tick_formatter2(
+ direction, lat_factor, levs)
+
+ return grid_info
+
+ def _get_raw_grid_lines(self,
+ lon_values, lat_values,
+ lon_min, lon_max, lat_min, lat_max):
+
+ lons_i = np.linspace(lon_min, lon_max, 100) # for interpolation
+ lats_i = np.linspace(lat_min, lat_max, 100)
+
+ lon_lines = [self.transform_xy(np.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 _clip_grid_lines_and_find_ticks(self, lines, values, levs, bb):
+ gi = {
+ "values": [],
+ "levels": [],
+ "tick_levels": dict(left=[], bottom=[], right=[], top=[]),
+ "tick_locs": dict(left=[], bottom=[], right=[], top=[]),
+ "lines": [],
+ }
+
+ tck_levels = gi["tick_levels"]
+ tck_locs = gi["tick_locs"]
+ for (lx, ly), v, lev in zip(lines, values, levs):
+ tcks = _find_line_box_crossings(np.column_stack([lx, ly]), bb)
+ gi["levels"].append(v)
+ gi["lines"].append([(lx, ly)])
+
+ for tck, direction in zip(tcks,
+ ["left", "right", "bottom", "top"]):
+ for t in tck:
+ tck_levels[direction].append(lev)
+ tck_locs[direction].append(t)
+
+ return gi
+
+ 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)]
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/grid_helper_curvelinear.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/grid_helper_curvelinear.py
new file mode 100644
index 0000000000..ae17452b6c
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/grid_helper_curvelinear.py
@@ -0,0 +1,336 @@
+"""
+An experimental support for curvilinear grid.
+"""
+
+import functools
+from itertools import chain
+
+import numpy as np
+
+import matplotlib as mpl
+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
+ g = self.grid_helper
+ ti1 = g.get_tick_iterator(self.nth_coord_ticks, side)
+ ti2 = g.get_tick_iterator(1-self.nth_coord_ticks, side, minor=True)
+ return chain(ti1, ti2), iter([])
+
+
+class FloatingAxisArtistHelper(_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.tick_formatter1(
+ "bottom", lon_factor, lon_levs),
+ "lat_labels": grid_finder.tick_formatter2(
+ "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 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_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.
+
+ 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
+
+ def get_tick_iterator(self, nth_coord, axis_side, minor=False):
+
+ # axisnr = dict(left=0, bottom=1, right=2, top=3)[axis_side]
+ angle_tangent = dict(left=90, right=90, bottom=0, top=0)[axis_side]
+ # angle = [0, 90, 180, 270][axisnr]
+ lon_or_lat = ["lon", "lat"][nth_coord]
+ if not minor: # major ticks
+ for (xy, a), l in zip(
+ self._grid_info[lon_or_lat]["tick_locs"][axis_side],
+ self._grid_info[lon_or_lat]["tick_labels"][axis_side]):
+ angle_normal = a
+ yield xy, angle_normal, angle_tangent, l
+ else:
+ for (xy, a), l in zip(
+ self._grid_info[lon_or_lat]["tick_locs"][axis_side],
+ self._grid_info[lon_or_lat]["tick_labels"][axis_side]):
+ angle_normal = a
+ yield xy, angle_normal, angle_tangent, ""
+ # for xy, a, l in self._grid_info[lon_or_lat]["ticks"][axis_side]:
+ # yield xy, a, ""
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/parasite_axes.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/parasite_axes.py
new file mode 100644
index 0000000000..4ebd6acc03
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/parasite_axes.py
@@ -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)