diff options
author | shumkovnd <shumkovnd@yandex-team.com> | 2023-11-10 14:39:34 +0300 |
---|---|---|
committer | shumkovnd <shumkovnd@yandex-team.com> | 2023-11-10 16:42:24 +0300 |
commit | 77eb2d3fdcec5c978c64e025ced2764c57c00285 (patch) | |
tree | c51edb0748ca8d4a08d7c7323312c27ba1a8b79a /contrib/python/matplotlib/py3/mpl_toolkits/axisartist | |
parent | dd6d20cadb65582270ac23f4b3b14ae189704b9d (diff) | |
download | ydb-77eb2d3fdcec5c978c64e025ced2764c57c00285.tar.gz |
KIKIMR-19287: add task_stats_drawing script
Diffstat (limited to 'contrib/python/matplotlib/py3/mpl_toolkits/axisartist')
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) |