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/axes_grid1/axes_divider.py | |
parent | dd6d20cadb65582270ac23f4b3b14ae189704b9d (diff) | |
download | ydb-77eb2d3fdcec5c978c64e025ced2764c57c00285.tar.gz |
KIKIMR-19287: add task_stats_drawing script
Diffstat (limited to 'contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/axes_divider.py')
-rw-r--r-- | contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/axes_divider.py | 694 |
1 files changed, 694 insertions, 0 deletions
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/axes_divider.py b/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/axes_divider.py new file mode 100644 index 0000000000..f6c38f35db --- /dev/null +++ b/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/axes_divider.py @@ -0,0 +1,694 @@ +""" +Helper classes to adjust the positions of multiple axes at drawing time. +""" + +import functools + +import numpy as np + +import matplotlib as mpl +from matplotlib import _api +from matplotlib.gridspec import SubplotSpec +import matplotlib.transforms as mtransforms +from . import axes_size as Size + + +class Divider: + """ + An Axes positioning class. + + The divider is initialized with lists of horizontal and vertical sizes + (:mod:`mpl_toolkits.axes_grid1.axes_size`) based on which a given + rectangular area will be divided. + + The `new_locator` method then creates a callable object + that can be used as the *axes_locator* of the axes. + """ + + def __init__(self, fig, pos, horizontal, vertical, + aspect=None, anchor="C"): + """ + Parameters + ---------- + fig : Figure + pos : tuple of 4 floats + Position of the rectangle that will be divided. + horizontal : list of :mod:`~mpl_toolkits.axes_grid1.axes_size` + Sizes for horizontal division. + vertical : list of :mod:`~mpl_toolkits.axes_grid1.axes_size` + Sizes for vertical division. + aspect : bool, optional + Whether overall rectangular area is reduced so that the relative + part of the horizontal and vertical scales have the same scale. + anchor : (float, float) or {'C', 'SW', 'S', 'SE', 'E', 'NE', 'N', \ +'NW', 'W'}, default: 'C' + Placement of the reduced rectangle, when *aspect* is True. + """ + + self._fig = fig + self._pos = pos + self._horizontal = horizontal + self._vertical = vertical + self._anchor = anchor + self.set_anchor(anchor) + self._aspect = aspect + self._xrefindex = 0 + self._yrefindex = 0 + self._locator = None + + def get_horizontal_sizes(self, renderer): + return np.array([s.get_size(renderer) for s in self.get_horizontal()]) + + def get_vertical_sizes(self, renderer): + return np.array([s.get_size(renderer) for s in self.get_vertical()]) + + def set_position(self, pos): + """ + Set the position of the rectangle. + + Parameters + ---------- + pos : tuple of 4 floats + position of the rectangle that will be divided + """ + self._pos = pos + + def get_position(self): + """Return the position of the rectangle.""" + return self._pos + + def set_anchor(self, anchor): + """ + Parameters + ---------- + anchor : (float, float) or {'C', 'SW', 'S', 'SE', 'E', 'NE', 'N', \ +'NW', 'W'} + Either an (*x*, *y*) pair of relative coordinates (0 is left or + bottom, 1 is right or top), 'C' (center), or a cardinal direction + ('SW', southwest, is bottom left, etc.). + + See Also + -------- + .Axes.set_anchor + """ + if isinstance(anchor, str): + _api.check_in_list(mtransforms.Bbox.coefs, anchor=anchor) + elif not isinstance(anchor, (tuple, list)) or len(anchor) != 2: + raise TypeError("anchor must be str or 2-tuple") + self._anchor = anchor + + def get_anchor(self): + """Return the anchor.""" + return self._anchor + + def get_subplotspec(self): + return None + + def set_horizontal(self, h): + """ + Parameters + ---------- + h : list of :mod:`~mpl_toolkits.axes_grid1.axes_size` + sizes for horizontal division + """ + self._horizontal = h + + def get_horizontal(self): + """Return horizontal sizes.""" + return self._horizontal + + def set_vertical(self, v): + """ + Parameters + ---------- + v : list of :mod:`~mpl_toolkits.axes_grid1.axes_size` + sizes for vertical division + """ + self._vertical = v + + def get_vertical(self): + """Return vertical sizes.""" + return self._vertical + + def set_aspect(self, aspect=False): + """ + Parameters + ---------- + aspect : bool + """ + self._aspect = aspect + + def get_aspect(self): + """Return aspect.""" + return self._aspect + + def set_locator(self, _locator): + self._locator = _locator + + def get_locator(self): + return self._locator + + def get_position_runtime(self, ax, renderer): + if self._locator is None: + return self.get_position() + else: + return self._locator(ax, renderer).bounds + + @staticmethod + def _calc_k(sizes, total): + # sizes is a (n, 2) array of (rel_size, abs_size); this method finds + # the k factor such that sum(rel_size * k + abs_size) == total. + rel_sum, abs_sum = sizes.sum(0) + return (total - abs_sum) / rel_sum if rel_sum else 0 + + @staticmethod + def _calc_offsets(sizes, k): + # Apply k factors to (n, 2) sizes array of (rel_size, abs_size); return + # the resulting cumulative offset positions. + return np.cumsum([0, *(sizes @ [k, 1])]) + + def new_locator(self, nx, ny, nx1=None, ny1=None): + """ + Return an axes locator callable for the specified cell. + + Parameters + ---------- + nx, nx1 : int + Integers specifying the column-position of the + cell. When *nx1* is None, a single *nx*-th column is + specified. Otherwise, location of columns spanning between *nx* + to *nx1* (but excluding *nx1*-th column) is specified. + ny, ny1 : int + Same as *nx* and *nx1*, but for row positions. + """ + if nx1 is None: + nx1 = nx + 1 + if ny1 is None: + ny1 = ny + 1 + # append_size("left") adds a new size at the beginning of the + # horizontal size lists; this shift transforms e.g. + # new_locator(nx=2, ...) into effectively new_locator(nx=3, ...). To + # take that into account, instead of recording nx, we record + # nx-self._xrefindex, where _xrefindex is shifted by 1 by each + # append_size("left"), and re-add self._xrefindex back to nx in + # _locate, when the actual axes position is computed. Ditto for y. + xref = self._xrefindex + yref = self._yrefindex + locator = functools.partial( + self._locate, nx - xref, ny - yref, nx1 - xref, ny1 - yref) + locator.get_subplotspec = self.get_subplotspec + return locator + + @_api.deprecated( + "3.8", alternative="divider.new_locator(...)(ax, renderer)") + def locate(self, nx, ny, nx1=None, ny1=None, axes=None, renderer=None): + """ + Implementation of ``divider.new_locator().__call__``. + + Parameters + ---------- + nx, nx1 : int + Integers specifying the column-position of the cell. When *nx1* is + None, a single *nx*-th column is specified. Otherwise, the + location of columns spanning between *nx* to *nx1* (but excluding + *nx1*-th column) is specified. + ny, ny1 : int + Same as *nx* and *nx1*, but for row positions. + axes + renderer + """ + xref = self._xrefindex + yref = self._yrefindex + return self._locate( + nx - xref, (nx + 1 if nx1 is None else nx1) - xref, + ny - yref, (ny + 1 if ny1 is None else ny1) - yref, + axes, renderer) + + def _locate(self, nx, ny, nx1, ny1, axes, renderer): + """ + Implementation of ``divider.new_locator().__call__``. + + The axes locator callable returned by ``new_locator()`` is created as + a `functools.partial` of this method with *nx*, *ny*, *nx1*, and *ny1* + specifying the requested cell. + """ + nx += self._xrefindex + nx1 += self._xrefindex + ny += self._yrefindex + ny1 += self._yrefindex + + fig_w, fig_h = self._fig.bbox.size / self._fig.dpi + x, y, w, h = self.get_position_runtime(axes, renderer) + + hsizes = self.get_horizontal_sizes(renderer) + vsizes = self.get_vertical_sizes(renderer) + k_h = self._calc_k(hsizes, fig_w * w) + k_v = self._calc_k(vsizes, fig_h * h) + + if self.get_aspect(): + k = min(k_h, k_v) + ox = self._calc_offsets(hsizes, k) + oy = self._calc_offsets(vsizes, k) + + ww = (ox[-1] - ox[0]) / fig_w + hh = (oy[-1] - oy[0]) / fig_h + pb = mtransforms.Bbox.from_bounds(x, y, w, h) + pb1 = mtransforms.Bbox.from_bounds(x, y, ww, hh) + x0, y0 = pb1.anchored(self.get_anchor(), pb).p0 + + else: + ox = self._calc_offsets(hsizes, k_h) + oy = self._calc_offsets(vsizes, k_v) + x0, y0 = x, y + + if nx1 is None: + nx1 = -1 + if ny1 is None: + ny1 = -1 + + x1, w1 = x0 + ox[nx] / fig_w, (ox[nx1] - ox[nx]) / fig_w + y1, h1 = y0 + oy[ny] / fig_h, (oy[ny1] - oy[ny]) / fig_h + + return mtransforms.Bbox.from_bounds(x1, y1, w1, h1) + + def append_size(self, position, size): + _api.check_in_list(["left", "right", "bottom", "top"], + position=position) + if position == "left": + self._horizontal.insert(0, size) + self._xrefindex += 1 + elif position == "right": + self._horizontal.append(size) + elif position == "bottom": + self._vertical.insert(0, size) + self._yrefindex += 1 + else: # 'top' + self._vertical.append(size) + + def add_auto_adjustable_area(self, use_axes, pad=0.1, adjust_dirs=None): + """ + Add auto-adjustable padding around *use_axes* to take their decorations + (title, labels, ticks, ticklabels) into account during layout. + + Parameters + ---------- + use_axes : `~matplotlib.axes.Axes` or list of `~matplotlib.axes.Axes` + The Axes whose decorations are taken into account. + pad : float, default: 0.1 + Additional padding in inches. + adjust_dirs : list of {"left", "right", "bottom", "top"}, optional + The sides where padding is added; defaults to all four sides. + """ + if adjust_dirs is None: + adjust_dirs = ["left", "right", "bottom", "top"] + for d in adjust_dirs: + self.append_size(d, Size._AxesDecorationsSize(use_axes, d) + pad) + + +@_api.deprecated("3.8") +class AxesLocator: + """ + A callable object which returns the position and size of a given + `.AxesDivider` cell. + """ + + def __init__(self, axes_divider, nx, ny, nx1=None, ny1=None): + """ + Parameters + ---------- + axes_divider : `~mpl_toolkits.axes_grid1.axes_divider.AxesDivider` + nx, nx1 : int + Integers specifying the column-position of the + cell. When *nx1* is None, a single *nx*-th column is + specified. Otherwise, location of columns spanning between *nx* + to *nx1* (but excluding *nx1*-th column) is specified. + ny, ny1 : int + Same as *nx* and *nx1*, but for row positions. + """ + self._axes_divider = axes_divider + + _xrefindex = axes_divider._xrefindex + _yrefindex = axes_divider._yrefindex + + self._nx, self._ny = nx - _xrefindex, ny - _yrefindex + + if nx1 is None: + nx1 = len(self._axes_divider) + if ny1 is None: + ny1 = len(self._axes_divider[0]) + + self._nx1 = nx1 - _xrefindex + self._ny1 = ny1 - _yrefindex + + def __call__(self, axes, renderer): + + _xrefindex = self._axes_divider._xrefindex + _yrefindex = self._axes_divider._yrefindex + + return self._axes_divider.locate(self._nx + _xrefindex, + self._ny + _yrefindex, + self._nx1 + _xrefindex, + self._ny1 + _yrefindex, + axes, + renderer) + + def get_subplotspec(self): + return self._axes_divider.get_subplotspec() + + +class SubplotDivider(Divider): + """ + The Divider class whose rectangle area is specified as a subplot geometry. + """ + + def __init__(self, fig, *args, horizontal=None, vertical=None, + aspect=None, anchor='C'): + """ + Parameters + ---------- + fig : `~matplotlib.figure.Figure` + + *args : tuple (*nrows*, *ncols*, *index*) or int + The array of subplots in the figure has dimensions ``(nrows, + ncols)``, and *index* is the index of the subplot being created. + *index* starts at 1 in the upper left corner and increases to the + right. + + If *nrows*, *ncols*, and *index* are all single digit numbers, then + *args* can be passed as a single 3-digit number (e.g. 234 for + (2, 3, 4)). + horizontal : list of :mod:`~mpl_toolkits.axes_grid1.axes_size`, optional + Sizes for horizontal division. + vertical : list of :mod:`~mpl_toolkits.axes_grid1.axes_size`, optional + Sizes for vertical division. + aspect : bool, optional + Whether overall rectangular area is reduced so that the relative + part of the horizontal and vertical scales have the same scale. + anchor : (float, float) or {'C', 'SW', 'S', 'SE', 'E', 'NE', 'N', \ +'NW', 'W'}, default: 'C' + Placement of the reduced rectangle, when *aspect* is True. + """ + self.figure = fig + super().__init__(fig, [0, 0, 1, 1], + horizontal=horizontal or [], vertical=vertical or [], + aspect=aspect, anchor=anchor) + self.set_subplotspec(SubplotSpec._from_subplot_args(fig, args)) + + def get_position(self): + """Return the bounds of the subplot box.""" + return self.get_subplotspec().get_position(self.figure).bounds + + def get_subplotspec(self): + """Get the SubplotSpec instance.""" + return self._subplotspec + + def set_subplotspec(self, subplotspec): + """Set the SubplotSpec instance.""" + self._subplotspec = subplotspec + self.set_position(subplotspec.get_position(self.figure)) + + +class AxesDivider(Divider): + """ + Divider based on the preexisting axes. + """ + + def __init__(self, axes, xref=None, yref=None): + """ + Parameters + ---------- + axes : :class:`~matplotlib.axes.Axes` + xref + yref + """ + self._axes = axes + if xref is None: + self._xref = Size.AxesX(axes) + else: + self._xref = xref + if yref is None: + self._yref = Size.AxesY(axes) + else: + self._yref = yref + + super().__init__(fig=axes.get_figure(), pos=None, + horizontal=[self._xref], vertical=[self._yref], + aspect=None, anchor="C") + + def _get_new_axes(self, *, axes_class=None, **kwargs): + axes = self._axes + if axes_class is None: + axes_class = type(axes) + return axes_class(axes.get_figure(), axes.get_position(original=True), + **kwargs) + + def new_horizontal(self, size, pad=None, pack_start=False, **kwargs): + """ + Helper method for ``append_axes("left")`` and ``append_axes("right")``. + + See the documentation of `append_axes` for more details. + + :meta private: + """ + if pad is None: + pad = mpl.rcParams["figure.subplot.wspace"] * self._xref + pos = "left" if pack_start else "right" + if pad: + if not isinstance(pad, Size._Base): + pad = Size.from_any(pad, fraction_ref=self._xref) + self.append_size(pos, pad) + if not isinstance(size, Size._Base): + size = Size.from_any(size, fraction_ref=self._xref) + self.append_size(pos, size) + locator = self.new_locator( + nx=0 if pack_start else len(self._horizontal) - 1, + ny=self._yrefindex) + ax = self._get_new_axes(**kwargs) + ax.set_axes_locator(locator) + return ax + + def new_vertical(self, size, pad=None, pack_start=False, **kwargs): + """ + Helper method for ``append_axes("top")`` and ``append_axes("bottom")``. + + See the documentation of `append_axes` for more details. + + :meta private: + """ + if pad is None: + pad = mpl.rcParams["figure.subplot.hspace"] * self._yref + pos = "bottom" if pack_start else "top" + if pad: + if not isinstance(pad, Size._Base): + pad = Size.from_any(pad, fraction_ref=self._yref) + self.append_size(pos, pad) + if not isinstance(size, Size._Base): + size = Size.from_any(size, fraction_ref=self._yref) + self.append_size(pos, size) + locator = self.new_locator( + nx=self._xrefindex, + ny=0 if pack_start else len(self._vertical) - 1) + ax = self._get_new_axes(**kwargs) + ax.set_axes_locator(locator) + return ax + + def append_axes(self, position, size, pad=None, *, axes_class=None, + **kwargs): + """ + Add a new axes on a given side of the main axes. + + Parameters + ---------- + position : {"left", "right", "bottom", "top"} + Where the new axes is positioned relative to the main axes. + size : :mod:`~mpl_toolkits.axes_grid1.axes_size` or float or str + The axes width or height. float or str arguments are interpreted + as ``axes_size.from_any(size, AxesX(<main_axes>))`` for left or + right axes, and likewise with ``AxesY`` for bottom or top axes. + pad : :mod:`~mpl_toolkits.axes_grid1.axes_size` or float or str + Padding between the axes. float or str arguments are interpreted + as for *size*. Defaults to :rc:`figure.subplot.wspace` times the + main Axes width (left or right axes) or :rc:`figure.subplot.hspace` + times the main Axes height (bottom or top axes). + axes_class : subclass type of `~.axes.Axes`, optional + The type of the new axes. Defaults to the type of the main axes. + **kwargs + All extra keywords arguments are passed to the created axes. + """ + create_axes, pack_start = _api.check_getitem({ + "left": (self.new_horizontal, True), + "right": (self.new_horizontal, False), + "bottom": (self.new_vertical, True), + "top": (self.new_vertical, False), + }, position=position) + ax = create_axes( + size, pad, pack_start=pack_start, axes_class=axes_class, **kwargs) + self._fig.add_axes(ax) + return ax + + def get_aspect(self): + if self._aspect is None: + aspect = self._axes.get_aspect() + if aspect == "auto": + return False + else: + return True + else: + return self._aspect + + def get_position(self): + if self._pos is None: + bbox = self._axes.get_position(original=True) + return bbox.bounds + else: + return self._pos + + def get_anchor(self): + if self._anchor is None: + return self._axes.get_anchor() + else: + return self._anchor + + def get_subplotspec(self): + return self._axes.get_subplotspec() + + +# Helper for HBoxDivider/VBoxDivider. +# The variable names are written for a horizontal layout, but the calculations +# work identically for vertical layouts. +def _locate(x, y, w, h, summed_widths, equal_heights, fig_w, fig_h, anchor): + + total_width = fig_w * w + max_height = fig_h * h + + # Determine the k factors. + n = len(equal_heights) + eq_rels, eq_abss = equal_heights.T + sm_rels, sm_abss = summed_widths.T + A = np.diag([*eq_rels, 0]) + A[:n, -1] = -1 + A[-1, :-1] = sm_rels + B = [*(-eq_abss), total_width - sm_abss.sum()] + # A @ K = B: This finds factors {k_0, ..., k_{N-1}, H} so that + # eq_rel_i * k_i + eq_abs_i = H for all i: all axes have the same height + # sum(sm_rel_i * k_i + sm_abs_i) = total_width: fixed total width + # (foo_rel_i * k_i + foo_abs_i will end up being the size of foo.) + *karray, height = np.linalg.solve(A, B) + if height > max_height: # Additionally, upper-bound the height. + karray = (max_height - eq_abss) / eq_rels + + # Compute the offsets corresponding to these factors. + ox = np.cumsum([0, *(sm_rels * karray + sm_abss)]) + ww = (ox[-1] - ox[0]) / fig_w + h0_rel, h0_abs = equal_heights[0] + hh = (karray[0]*h0_rel + h0_abs) / fig_h + pb = mtransforms.Bbox.from_bounds(x, y, w, h) + pb1 = mtransforms.Bbox.from_bounds(x, y, ww, hh) + x0, y0 = pb1.anchored(anchor, pb).p0 + + return x0, y0, ox, hh + + +class HBoxDivider(SubplotDivider): + """ + A `.SubplotDivider` for laying out axes horizontally, while ensuring that + they have equal heights. + + Examples + -------- + .. plot:: gallery/axes_grid1/demo_axes_hbox_divider.py + """ + + def new_locator(self, nx, nx1=None): + """ + Create an axes locator callable for the specified cell. + + Parameters + ---------- + nx, nx1 : int + Integers specifying the column-position of the + cell. When *nx1* is None, a single *nx*-th column is + specified. Otherwise, location of columns spanning between *nx* + to *nx1* (but excluding *nx1*-th column) is specified. + """ + return super().new_locator(nx, 0, nx1, 0) + + def _locate(self, nx, ny, nx1, ny1, axes, renderer): + # docstring inherited + nx += self._xrefindex + nx1 += self._xrefindex + fig_w, fig_h = self._fig.bbox.size / self._fig.dpi + x, y, w, h = self.get_position_runtime(axes, renderer) + summed_ws = self.get_horizontal_sizes(renderer) + equal_hs = self.get_vertical_sizes(renderer) + x0, y0, ox, hh = _locate( + x, y, w, h, summed_ws, equal_hs, fig_w, fig_h, self.get_anchor()) + if nx1 is None: + nx1 = -1 + x1, w1 = x0 + ox[nx] / fig_w, (ox[nx1] - ox[nx]) / fig_w + y1, h1 = y0, hh + return mtransforms.Bbox.from_bounds(x1, y1, w1, h1) + + +class VBoxDivider(SubplotDivider): + """ + A `.SubplotDivider` for laying out axes vertically, while ensuring that + they have equal widths. + """ + + def new_locator(self, ny, ny1=None): + """ + Create an axes locator callable for the specified cell. + + Parameters + ---------- + ny, ny1 : int + Integers specifying the row-position of the + cell. When *ny1* is None, a single *ny*-th row is + specified. Otherwise, location of rows spanning between *ny* + to *ny1* (but excluding *ny1*-th row) is specified. + """ + return super().new_locator(0, ny, 0, ny1) + + def _locate(self, nx, ny, nx1, ny1, axes, renderer): + # docstring inherited + ny += self._yrefindex + ny1 += self._yrefindex + fig_w, fig_h = self._fig.bbox.size / self._fig.dpi + x, y, w, h = self.get_position_runtime(axes, renderer) + summed_hs = self.get_vertical_sizes(renderer) + equal_ws = self.get_horizontal_sizes(renderer) + y0, x0, oy, ww = _locate( + y, x, h, w, summed_hs, equal_ws, fig_h, fig_w, self.get_anchor()) + if ny1 is None: + ny1 = -1 + x1, w1 = x0, ww + y1, h1 = y0 + oy[ny] / fig_h, (oy[ny1] - oy[ny]) / fig_h + return mtransforms.Bbox.from_bounds(x1, y1, w1, h1) + + +def make_axes_locatable(axes): + divider = AxesDivider(axes) + locator = divider.new_locator(nx=0, ny=0) + axes.set_axes_locator(locator) + + return divider + + +def make_axes_area_auto_adjustable( + ax, use_axes=None, pad=0.1, adjust_dirs=None): + """ + Add auto-adjustable padding around *ax* to take its decorations (title, + labels, ticks, ticklabels) into account during layout, using + `.Divider.add_auto_adjustable_area`. + + By default, padding is determined from the decorations of *ax*. + Pass *use_axes* to consider the decorations of other Axes instead. + """ + if adjust_dirs is None: + adjust_dirs = ["left", "right", "bottom", "top"] + divider = make_axes_locatable(ax) + if use_axes is None: + use_axes = ax + divider.add_auto_adjustable_area(use_axes=use_axes, pad=pad, + adjust_dirs=adjust_dirs) |