gridspec.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836
  1. r"""
  2. :mod:`~matplotlib.gridspec` contains classes that help to layout multiple
  3. `~.axes.Axes` in a grid-like pattern within a figure.
  4. The `GridSpec` specifies the overall grid structure. Individual cells within
  5. the grid are referenced by `SubplotSpec`\s.
  6. See the tutorial :ref:`sphx_glr_tutorials_intermediate_gridspec.py` for a
  7. comprehensive usage guide.
  8. """
  9. import copy
  10. import logging
  11. from numbers import Integral
  12. import numpy as np
  13. import matplotlib as mpl
  14. from matplotlib import _pylab_helpers, cbook, tight_layout, rcParams
  15. from matplotlib.transforms import Bbox
  16. import matplotlib._layoutbox as layoutbox
  17. _log = logging.getLogger(__name__)
  18. class GridSpecBase:
  19. """
  20. A base class of GridSpec that specifies the geometry of the grid
  21. that a subplot will be placed.
  22. """
  23. def __init__(self, nrows, ncols, height_ratios=None, width_ratios=None):
  24. """
  25. Parameters
  26. ----------
  27. nrows, ncols : int
  28. The number of rows and columns of the grid.
  29. width_ratios : array-like of length *ncols*, optional
  30. Defines the relative widths of the columns. Each column gets a
  31. relative width of ``width_ratios[i] / sum(width_ratios)``.
  32. If not given, all columns will have the same width.
  33. height_ratios : array-like of length *nrows*, optional
  34. Defines the relative heights of the rows. Each column gets a
  35. relative height of ``height_ratios[i] / sum(height_ratios)``.
  36. If not given, all rows will have the same height.
  37. """
  38. if not isinstance(nrows, Integral) or nrows <= 0:
  39. raise ValueError(
  40. f"Number of rows must be a positive integer, not {nrows}")
  41. if not isinstance(ncols, Integral) or ncols <= 0:
  42. raise ValueError(
  43. f"Number of columns must be a positive integer, not {ncols}")
  44. self._nrows, self._ncols = nrows, ncols
  45. self.set_height_ratios(height_ratios)
  46. self.set_width_ratios(width_ratios)
  47. def __repr__(self):
  48. height_arg = (', height_ratios=%r' % (self._row_height_ratios,)
  49. if self._row_height_ratios is not None else '')
  50. width_arg = (', width_ratios=%r' % (self._col_width_ratios,)
  51. if self._col_width_ratios is not None else '')
  52. return '{clsname}({nrows}, {ncols}{optionals})'.format(
  53. clsname=self.__class__.__name__,
  54. nrows=self._nrows,
  55. ncols=self._ncols,
  56. optionals=height_arg + width_arg,
  57. )
  58. nrows = property(lambda self: self._nrows,
  59. doc="The number of rows in the grid.")
  60. ncols = property(lambda self: self._ncols,
  61. doc="The number of columns in the grid.")
  62. def get_geometry(self):
  63. """
  64. Return a tuple containing the number of rows and columns in the grid.
  65. """
  66. return self._nrows, self._ncols
  67. def get_subplot_params(self, figure=None):
  68. # Must be implemented in subclasses
  69. pass
  70. def new_subplotspec(self, loc, rowspan=1, colspan=1):
  71. """
  72. Create and return a `.SubplotSpec` instance.
  73. Parameters
  74. ----------
  75. loc : (int, int)
  76. The position of the subplot in the grid as
  77. ``(row_index, column_index)``.
  78. rowspan, colspan : int, default: 1
  79. The number of rows and columns the subplot should span in the grid.
  80. """
  81. loc1, loc2 = loc
  82. subplotspec = self[loc1:loc1+rowspan, loc2:loc2+colspan]
  83. return subplotspec
  84. def set_width_ratios(self, width_ratios):
  85. """
  86. Set the relative widths of the columns.
  87. *width_ratios* must be of length *ncols*. Each column gets a relative
  88. width of ``width_ratios[i] / sum(width_ratios)``.
  89. """
  90. if width_ratios is not None and len(width_ratios) != self._ncols:
  91. raise ValueError('Expected the given number of width ratios to '
  92. 'match the number of columns of the grid')
  93. self._col_width_ratios = width_ratios
  94. def get_width_ratios(self):
  95. """
  96. Return the width ratios.
  97. This is *None* if no width ratios have been set explicitly.
  98. """
  99. return self._col_width_ratios
  100. def set_height_ratios(self, height_ratios):
  101. """
  102. Set the relative heights of the rows.
  103. *height_ratios* must be of length *nrows*. Each row gets a relative
  104. height of ``height_ratios[i] / sum(height_ratios)``.
  105. """
  106. if height_ratios is not None and len(height_ratios) != self._nrows:
  107. raise ValueError('Expected the given number of height ratios to '
  108. 'match the number of rows of the grid')
  109. self._row_height_ratios = height_ratios
  110. def get_height_ratios(self):
  111. """
  112. Return the height ratios.
  113. This is *None* if no height ratios have been set explicitly.
  114. """
  115. return self._row_height_ratios
  116. def get_grid_positions(self, fig, raw=False):
  117. """
  118. Return the positions of the grid cells in figure coordinates.
  119. Parameters
  120. ----------
  121. fig : `~matplotlib.figure.Figure`
  122. The figure the grid should be applied to. The subplot parameters
  123. (margins and spacing between subplots) are taken from *fig*.
  124. raw : bool, default: False
  125. If *True*, the subplot parameters of the figure are not taken
  126. into account. The grid spans the range [0, 1] in both directions
  127. without margins and there is no space between grid cells. This is
  128. used for constrained_layout.
  129. Returns
  130. -------
  131. bottoms, tops, lefts, rights : array
  132. The bottom, top, left, right positions of the grid cells in
  133. figure coordinates.
  134. """
  135. nrows, ncols = self.get_geometry()
  136. if raw:
  137. left = 0.
  138. right = 1.
  139. bottom = 0.
  140. top = 1.
  141. wspace = 0.
  142. hspace = 0.
  143. else:
  144. subplot_params = self.get_subplot_params(fig)
  145. left = subplot_params.left
  146. right = subplot_params.right
  147. bottom = subplot_params.bottom
  148. top = subplot_params.top
  149. wspace = subplot_params.wspace
  150. hspace = subplot_params.hspace
  151. tot_width = right - left
  152. tot_height = top - bottom
  153. # calculate accumulated heights of columns
  154. cell_h = tot_height / (nrows + hspace*(nrows-1))
  155. sep_h = hspace * cell_h
  156. if self._row_height_ratios is not None:
  157. norm = cell_h * nrows / sum(self._row_height_ratios)
  158. cell_heights = [r * norm for r in self._row_height_ratios]
  159. else:
  160. cell_heights = [cell_h] * nrows
  161. sep_heights = [0] + ([sep_h] * (nrows-1))
  162. cell_hs = np.cumsum(np.column_stack([sep_heights, cell_heights]).flat)
  163. # calculate accumulated widths of rows
  164. cell_w = tot_width / (ncols + wspace*(ncols-1))
  165. sep_w = wspace * cell_w
  166. if self._col_width_ratios is not None:
  167. norm = cell_w * ncols / sum(self._col_width_ratios)
  168. cell_widths = [r * norm for r in self._col_width_ratios]
  169. else:
  170. cell_widths = [cell_w] * ncols
  171. sep_widths = [0] + ([sep_w] * (ncols-1))
  172. cell_ws = np.cumsum(np.column_stack([sep_widths, cell_widths]).flat)
  173. fig_tops, fig_bottoms = (top - cell_hs).reshape((-1, 2)).T
  174. fig_lefts, fig_rights = (left + cell_ws).reshape((-1, 2)).T
  175. return fig_bottoms, fig_tops, fig_lefts, fig_rights
  176. def __getitem__(self, key):
  177. """Create and return a `.SubplotSpec` instance."""
  178. nrows, ncols = self.get_geometry()
  179. def _normalize(key, size, axis): # Includes last index.
  180. orig_key = key
  181. if isinstance(key, slice):
  182. start, stop, _ = key.indices(size)
  183. if stop > start:
  184. return start, stop - 1
  185. raise IndexError("GridSpec slice would result in no space "
  186. "allocated for subplot")
  187. else:
  188. if key < 0:
  189. key = key + size
  190. if 0 <= key < size:
  191. return key, key
  192. elif axis is not None:
  193. raise IndexError(f"index {orig_key} is out of bounds for "
  194. f"axis {axis} with size {size}")
  195. else: # flat index
  196. raise IndexError(f"index {orig_key} is out of bounds for "
  197. f"GridSpec with size {size}")
  198. if isinstance(key, tuple):
  199. try:
  200. k1, k2 = key
  201. except ValueError as err:
  202. raise ValueError("Unrecognized subplot spec") from err
  203. num1, num2 = np.ravel_multi_index(
  204. [_normalize(k1, nrows, 0), _normalize(k2, ncols, 1)],
  205. (nrows, ncols))
  206. else: # Single key
  207. num1, num2 = _normalize(key, nrows * ncols, None)
  208. return SubplotSpec(self, num1, num2)
  209. def subplots(self, *, sharex=False, sharey=False, squeeze=True,
  210. subplot_kw=None):
  211. """
  212. Add all subplots specified by this `GridSpec` to its parent figure.
  213. This utility wrapper makes it convenient to create common layouts of
  214. subplots in a single call.
  215. Parameters
  216. ----------
  217. sharex, sharey : bool or {'none', 'all', 'row', 'col'}, default: False
  218. Controls sharing of properties among x (*sharex*) or y (*sharey*)
  219. axes:
  220. - True or 'all': x- or y-axis will be shared among all subplots.
  221. - False or 'none': each subplot x- or y-axis will be independent.
  222. - 'row': each subplot row will share an x- or y-axis.
  223. - 'col': each subplot column will share an x- or y-axis.
  224. When subplots have a shared x-axis along a column, only the x tick
  225. labels of the bottom subplot are created. Similarly, when subplots
  226. have a shared y-axis along a row, only the y tick labels of the
  227. first column subplot are created. To later turn other subplots'
  228. ticklabels on, use `~matplotlib.axes.Axes.tick_params`.
  229. squeeze : bool, optional, default: True
  230. - If True, extra dimensions are squeezed out from the returned
  231. array of Axes:
  232. - if only one subplot is constructed (nrows=ncols=1), the
  233. resulting single Axes object is returned as a scalar.
  234. - for Nx1 or 1xM subplots, the returned object is a 1D numpy
  235. object array of Axes objects.
  236. - for NxM, subplots with N>1 and M>1 are returned as a 2D array.
  237. - If False, no squeezing at all is done: the returned Axes object
  238. is always a 2D array containing Axes instances, even if it ends
  239. up being 1x1.
  240. subplot_kw : dict, optional
  241. Dict with keywords passed to the `~.Figure.add_subplot` call used
  242. to create each subplot.
  243. Returns
  244. -------
  245. ax : `~.axes.Axes` object or array of Axes objects.
  246. *ax* can be either a single `~matplotlib.axes.Axes` object or
  247. an array of Axes objects if more than one subplot was created. The
  248. dimensions of the resulting array can be controlled with the
  249. squeeze keyword, see above.
  250. See Also
  251. --------
  252. .pyplot.subplots
  253. .Figure.add_subplot
  254. .pyplot.subplot
  255. """
  256. figure = self.figure
  257. if figure is None:
  258. raise ValueError("GridSpec.subplots() only works for GridSpecs "
  259. "created with a parent figure")
  260. if isinstance(sharex, bool):
  261. sharex = "all" if sharex else "none"
  262. if isinstance(sharey, bool):
  263. sharey = "all" if sharey else "none"
  264. # This check was added because it is very easy to type
  265. # `subplots(1, 2, 1)` when `subplot(1, 2, 1)` was intended.
  266. # In most cases, no error will ever occur, but mysterious behavior
  267. # will result because what was intended to be the subplot index is
  268. # instead treated as a bool for sharex.
  269. if isinstance(sharex, Integral):
  270. cbook._warn_external(
  271. "sharex argument to subplots() was an integer. Did you "
  272. "intend to use subplot() (without 's')?")
  273. cbook._check_in_list(["all", "row", "col", "none"],
  274. sharex=sharex, sharey=sharey)
  275. if subplot_kw is None:
  276. subplot_kw = {}
  277. # don't mutate kwargs passed by user...
  278. subplot_kw = subplot_kw.copy()
  279. # Create array to hold all axes.
  280. axarr = np.empty((self._nrows, self._ncols), dtype=object)
  281. for row in range(self._nrows):
  282. for col in range(self._ncols):
  283. shared_with = {"none": None, "all": axarr[0, 0],
  284. "row": axarr[row, 0], "col": axarr[0, col]}
  285. subplot_kw["sharex"] = shared_with[sharex]
  286. subplot_kw["sharey"] = shared_with[sharey]
  287. axarr[row, col] = figure.add_subplot(
  288. self[row, col], **subplot_kw)
  289. # turn off redundant tick labeling
  290. if sharex in ["col", "all"]:
  291. # turn off all but the bottom row
  292. for ax in axarr[:-1, :].flat:
  293. ax.xaxis.set_tick_params(which='both',
  294. labelbottom=False, labeltop=False)
  295. ax.xaxis.offsetText.set_visible(False)
  296. if sharey in ["row", "all"]:
  297. # turn off all but the first column
  298. for ax in axarr[:, 1:].flat:
  299. ax.yaxis.set_tick_params(which='both',
  300. labelleft=False, labelright=False)
  301. ax.yaxis.offsetText.set_visible(False)
  302. if squeeze:
  303. # Discarding unneeded dimensions that equal 1. If we only have one
  304. # subplot, just return it instead of a 1-element array.
  305. return axarr.item() if axarr.size == 1 else axarr.squeeze()
  306. else:
  307. # Returned axis array will be always 2-d, even if nrows=ncols=1.
  308. return axarr
  309. class GridSpec(GridSpecBase):
  310. """
  311. A grid layout to place subplots within a figure.
  312. The location of the grid cells is determined in a similar way to
  313. `~.figure.SubplotParams` using *left*, *right*, *top*, *bottom*, *wspace*
  314. and *hspace*.
  315. """
  316. def __init__(self, nrows, ncols, figure=None,
  317. left=None, bottom=None, right=None, top=None,
  318. wspace=None, hspace=None,
  319. width_ratios=None, height_ratios=None):
  320. """
  321. Parameters
  322. ----------
  323. nrows, ncols : int
  324. The number of rows and columns of the grid.
  325. figure : `~.figure.Figure`, optional
  326. Only used for constrained layout to create a proper layoutbox.
  327. left, right, top, bottom : float, optional
  328. Extent of the subplots as a fraction of figure width or height.
  329. Left cannot be larger than right, and bottom cannot be larger than
  330. top. If not given, the values will be inferred from a figure or
  331. rcParams at draw time. See also `GridSpec.get_subplot_params`.
  332. wspace : float, optional
  333. The amount of width reserved for space between subplots,
  334. expressed as a fraction of the average axis width.
  335. If not given, the values will be inferred from a figure or
  336. rcParams when necessary. See also `GridSpec.get_subplot_params`.
  337. hspace : float, optional
  338. The amount of height reserved for space between subplots,
  339. expressed as a fraction of the average axis height.
  340. If not given, the values will be inferred from a figure or
  341. rcParams when necessary. See also `GridSpec.get_subplot_params`.
  342. width_ratios : array-like of length *ncols*, optional
  343. Defines the relative widths of the columns. Each column gets a
  344. relative width of ``width_ratios[i] / sum(width_ratios)``.
  345. If not given, all columns will have the same width.
  346. height_ratios : array-like of length *nrows*, optional
  347. Defines the relative heights of the rows. Each column gets a
  348. relative height of ``height_ratios[i] / sum(height_ratios)``.
  349. If not given, all rows will have the same height.
  350. """
  351. self.left = left
  352. self.bottom = bottom
  353. self.right = right
  354. self.top = top
  355. self.wspace = wspace
  356. self.hspace = hspace
  357. self.figure = figure
  358. GridSpecBase.__init__(self, nrows, ncols,
  359. width_ratios=width_ratios,
  360. height_ratios=height_ratios)
  361. if self.figure is None or not self.figure.get_constrained_layout():
  362. self._layoutbox = None
  363. else:
  364. self.figure.init_layoutbox()
  365. self._layoutbox = layoutbox.LayoutBox(
  366. parent=self.figure._layoutbox,
  367. name='gridspec' + layoutbox.seq_id(),
  368. artist=self)
  369. # by default the layoutbox for a gridspec will fill a figure.
  370. # but this can change below if the gridspec is created from a
  371. # subplotspec. (GridSpecFromSubplotSpec)
  372. _AllowedKeys = ["left", "bottom", "right", "top", "wspace", "hspace"]
  373. def __getstate__(self):
  374. return {**self.__dict__, "_layoutbox": None}
  375. def update(self, **kwargs):
  376. """
  377. Update the subplot parameters of the grid.
  378. Parameters that are not explicitly given are not changed. Setting a
  379. parameter to *None* resets it to :rc:`figure.subplot.*`.
  380. Parameters
  381. ----------
  382. left, right, top, bottom : float or None, optional
  383. Extent of the subplots as a fraction of figure width or height.
  384. wspace, hspace : float, optional
  385. Spacing between the subplots as a fraction of the average subplot
  386. width / height.
  387. """
  388. for k, v in kwargs.items():
  389. if k in self._AllowedKeys:
  390. setattr(self, k, v)
  391. else:
  392. raise AttributeError(f"{k} is an unknown keyword")
  393. for figmanager in _pylab_helpers.Gcf.figs.values():
  394. for ax in figmanager.canvas.figure.axes:
  395. # copied from Figure.subplots_adjust
  396. if not isinstance(ax, mpl.axes.SubplotBase):
  397. # Check if sharing a subplots axis
  398. if isinstance(ax._sharex, mpl.axes.SubplotBase):
  399. if ax._sharex.get_subplotspec().get_gridspec() == self:
  400. ax._sharex.update_params()
  401. ax._set_position(ax._sharex.figbox)
  402. elif isinstance(ax._sharey, mpl.axes.SubplotBase):
  403. if ax._sharey.get_subplotspec().get_gridspec() == self:
  404. ax._sharey.update_params()
  405. ax._set_position(ax._sharey.figbox)
  406. else:
  407. ss = ax.get_subplotspec().get_topmost_subplotspec()
  408. if ss.get_gridspec() == self:
  409. ax.update_params()
  410. ax._set_position(ax.figbox)
  411. def get_subplot_params(self, figure=None):
  412. """
  413. Return the `~.SubplotParams` for the GridSpec.
  414. In order of precedence the values are taken from
  415. - non-*None* attributes of the GridSpec
  416. - the provided *figure*
  417. - :rc:`figure.subplot.*`
  418. """
  419. if figure is None:
  420. kw = {k: rcParams["figure.subplot."+k] for k in self._AllowedKeys}
  421. subplotpars = mpl.figure.SubplotParams(**kw)
  422. else:
  423. subplotpars = copy.copy(figure.subplotpars)
  424. subplotpars.update(**{k: getattr(self, k) for k in self._AllowedKeys})
  425. return subplotpars
  426. def locally_modified_subplot_params(self):
  427. """
  428. Return a list of the names of the subplot parameters explicitly set
  429. in the GridSpec.
  430. This is a subset of the attributes of `.SubplotParams`.
  431. """
  432. return [k for k in self._AllowedKeys if getattr(self, k)]
  433. def tight_layout(self, figure, renderer=None,
  434. pad=1.08, h_pad=None, w_pad=None, rect=None):
  435. """
  436. Adjust subplot parameters to give specified padding.
  437. Parameters
  438. ----------
  439. pad : float
  440. Padding between the figure edge and the edges of subplots, as a
  441. fraction of the font-size.
  442. h_pad, w_pad : float, optional
  443. Padding (height/width) between edges of adjacent subplots.
  444. Defaults to *pad*.
  445. rect : tuple of 4 floats, default: (0, 0, 1, 1), i.e. the whole figure
  446. (left, bottom, right, top) rectangle in normalized figure
  447. coordinates that the whole subplots area (including labels) will
  448. fit into.
  449. """
  450. subplotspec_list = tight_layout.get_subplotspec_list(
  451. figure.axes, grid_spec=self)
  452. if None in subplotspec_list:
  453. cbook._warn_external("This figure includes Axes that are not "
  454. "compatible with tight_layout, so results "
  455. "might be incorrect.")
  456. if renderer is None:
  457. renderer = tight_layout.get_renderer(figure)
  458. kwargs = tight_layout.get_tight_layout_figure(
  459. figure, figure.axes, subplotspec_list, renderer,
  460. pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
  461. if kwargs:
  462. self.update(**kwargs)
  463. class GridSpecFromSubplotSpec(GridSpecBase):
  464. """
  465. GridSpec whose subplot layout parameters are inherited from the
  466. location specified by a given SubplotSpec.
  467. """
  468. def __init__(self, nrows, ncols,
  469. subplot_spec,
  470. wspace=None, hspace=None,
  471. height_ratios=None, width_ratios=None):
  472. """
  473. The number of rows and number of columns of the grid need to
  474. be set. An instance of SubplotSpec is also needed to be set
  475. from which the layout parameters will be inherited. The wspace
  476. and hspace of the layout can be optionally specified or the
  477. default values (from the figure or rcParams) will be used.
  478. """
  479. self._wspace = wspace
  480. self._hspace = hspace
  481. self._subplot_spec = subplot_spec
  482. self.figure = self._subplot_spec.get_gridspec().figure
  483. GridSpecBase.__init__(self, nrows, ncols,
  484. width_ratios=width_ratios,
  485. height_ratios=height_ratios)
  486. # do the layoutboxes
  487. subspeclb = subplot_spec._layoutbox
  488. if subspeclb is None:
  489. self._layoutbox = None
  490. else:
  491. # OK, this is needed to divide the figure.
  492. self._layoutbox = subspeclb.layout_from_subplotspec(
  493. subplot_spec,
  494. name=subspeclb.name + '.gridspec' + layoutbox.seq_id(),
  495. artist=self)
  496. def get_subplot_params(self, figure=None):
  497. """Return a dictionary of subplot layout parameters."""
  498. hspace = (self._hspace if self._hspace is not None
  499. else figure.subplotpars.hspace if figure is not None
  500. else rcParams["figure.subplot.hspace"])
  501. wspace = (self._wspace if self._wspace is not None
  502. else figure.subplotpars.wspace if figure is not None
  503. else rcParams["figure.subplot.wspace"])
  504. figbox = self._subplot_spec.get_position(figure)
  505. left, bottom, right, top = figbox.extents
  506. return mpl.figure.SubplotParams(left=left, right=right,
  507. bottom=bottom, top=top,
  508. wspace=wspace, hspace=hspace)
  509. def get_topmost_subplotspec(self):
  510. """
  511. Return the topmost `.SubplotSpec` instance associated with the subplot.
  512. """
  513. return self._subplot_spec.get_topmost_subplotspec()
  514. class SubplotSpec:
  515. """
  516. Specifies the location of a subplot in a `GridSpec`.
  517. .. note::
  518. Likely, you'll never instantiate a `SubplotSpec` yourself. Instead you
  519. will typically obtain one from a `GridSpec` using item-access.
  520. Parameters
  521. ----------
  522. gridspec : `~matplotlib.gridspec.GridSpec`
  523. The GridSpec, which the subplot is referencing.
  524. num1, num2 : int
  525. The subplot will occupy the num1-th cell of the given
  526. gridspec. If num2 is provided, the subplot will span between
  527. num1-th cell and num2-th cell *inclusive*.
  528. The index starts from 0.
  529. """
  530. def __init__(self, gridspec, num1, num2=None):
  531. self._gridspec = gridspec
  532. self.num1 = num1
  533. self.num2 = num2
  534. if gridspec._layoutbox is not None:
  535. glb = gridspec._layoutbox
  536. # So note that here we don't assign any layout yet,
  537. # just make the layoutbox that will contain all items
  538. # associated w/ this axis. This can include other axes like
  539. # a colorbar or a legend.
  540. self._layoutbox = layoutbox.LayoutBox(
  541. parent=glb,
  542. name=glb.name + '.ss' + layoutbox.seq_id(),
  543. artist=self)
  544. else:
  545. self._layoutbox = None
  546. def __repr__(self):
  547. return (f"{self.get_gridspec()}["
  548. f"{self.rowspan.start}:{self.rowspan.stop}, "
  549. f"{self.colspan.start}:{self.colspan.stop}]")
  550. @staticmethod
  551. def _from_subplot_args(figure, args):
  552. """
  553. Construct a `.SubplotSpec` from a parent `.Figure` and either
  554. - a `.SubplotSpec` -- returned as is;
  555. - one or three numbers -- a MATLAB-style subplot specifier.
  556. """
  557. message = ("Passing non-integers as three-element position "
  558. "specification is deprecated since %(since)s and will be "
  559. "removed %(removal)s.")
  560. if len(args) == 1:
  561. arg, = args
  562. if isinstance(arg, SubplotSpec):
  563. return arg
  564. else:
  565. if not isinstance(arg, Integral):
  566. cbook.warn_deprecated("3.3", message=message)
  567. arg = str(arg)
  568. try:
  569. rows, cols, num = map(int, str(arg))
  570. except ValueError:
  571. raise ValueError(
  572. f"Single argument to subplot must be a three-digit "
  573. f"integer, not {arg}") from None
  574. # num - 1 for converting from MATLAB to python indexing
  575. return GridSpec(rows, cols, figure=figure)[num - 1]
  576. elif len(args) == 3:
  577. rows, cols, num = args
  578. if not (isinstance(rows, Integral) and isinstance(cols, Integral)):
  579. cbook.warn_deprecated("3.3", message=message)
  580. rows, cols = map(int, [rows, cols])
  581. gs = GridSpec(rows, cols, figure=figure)
  582. if isinstance(num, tuple) and len(num) == 2:
  583. if not all(isinstance(n, Integral) for n in num):
  584. cbook.warn_deprecated("3.3", message=message)
  585. i, j = map(int, num)
  586. else:
  587. i, j = num
  588. return gs[i-1:j]
  589. else:
  590. if not isinstance(num, Integral):
  591. cbook.warn_deprecated("3.3", message=message)
  592. num = int(num)
  593. if num < 1 or num > rows*cols:
  594. raise ValueError(
  595. f"num must be 1 <= num <= {rows*cols}, not {num}")
  596. return gs[num - 1] # -1 due to MATLAB indexing.
  597. else:
  598. raise TypeError(f"subplot() takes 1 or 3 positional arguments but "
  599. f"{len(args)} were given")
  600. # num2 is a property only to handle the case where it is None and someone
  601. # mutates num1.
  602. @property
  603. def num2(self):
  604. return self.num1 if self._num2 is None else self._num2
  605. @num2.setter
  606. def num2(self, value):
  607. self._num2 = value
  608. def __getstate__(self):
  609. return {**self.__dict__, "_layoutbox": None}
  610. def get_gridspec(self):
  611. return self._gridspec
  612. def get_geometry(self):
  613. """
  614. Return the subplot geometry as tuple ``(n_rows, n_cols, start, stop)``.
  615. The indices *start* and *stop* define the range of the subplot within
  616. the `GridSpec`. *stop* is inclusive (i.e. for a single cell
  617. ``start == stop``).
  618. """
  619. rows, cols = self.get_gridspec().get_geometry()
  620. return rows, cols, self.num1, self.num2
  621. @cbook.deprecated("3.3", alternative="rowspan, colspan")
  622. def get_rows_columns(self):
  623. """
  624. Return the subplot row and column numbers as a tuple
  625. ``(n_rows, n_cols, row_start, row_stop, col_start, col_stop)``.
  626. """
  627. gridspec = self.get_gridspec()
  628. nrows, ncols = gridspec.get_geometry()
  629. row_start, col_start = divmod(self.num1, ncols)
  630. row_stop, col_stop = divmod(self.num2, ncols)
  631. return nrows, ncols, row_start, row_stop, col_start, col_stop
  632. @property
  633. def rowspan(self):
  634. """The rows spanned by this subplot, as a `range` object."""
  635. ncols = self.get_gridspec().ncols
  636. return range(self.num1 // ncols, self.num2 // ncols + 1)
  637. @property
  638. def colspan(self):
  639. """The columns spanned by this subplot, as a `range` object."""
  640. ncols = self.get_gridspec().ncols
  641. # We explicitly support num2 refering to a column on num1's *left*, so
  642. # we must sort the column indices here so that the range makes sense.
  643. c1, c2 = sorted([self.num1 % ncols, self.num2 % ncols])
  644. return range(c1, c2 + 1)
  645. def get_position(self, figure, return_all=False):
  646. """
  647. Update the subplot position from ``figure.subplotpars``.
  648. """
  649. gridspec = self.get_gridspec()
  650. nrows, ncols = gridspec.get_geometry()
  651. rows, cols = np.unravel_index([self.num1, self.num2], (nrows, ncols))
  652. fig_bottoms, fig_tops, fig_lefts, fig_rights = \
  653. gridspec.get_grid_positions(figure)
  654. fig_bottom = fig_bottoms[rows].min()
  655. fig_top = fig_tops[rows].max()
  656. fig_left = fig_lefts[cols].min()
  657. fig_right = fig_rights[cols].max()
  658. figbox = Bbox.from_extents(fig_left, fig_bottom, fig_right, fig_top)
  659. if return_all:
  660. return figbox, rows[0], cols[0], nrows, ncols
  661. else:
  662. return figbox
  663. def get_topmost_subplotspec(self):
  664. """
  665. Return the topmost `SubplotSpec` instance associated with the subplot.
  666. """
  667. gridspec = self.get_gridspec()
  668. if hasattr(gridspec, "get_topmost_subplotspec"):
  669. return gridspec.get_topmost_subplotspec()
  670. else:
  671. return self
  672. def __eq__(self, other):
  673. """
  674. Two SubplotSpecs are considered equal if they refer to the same
  675. position(s) in the same `GridSpec`.
  676. """
  677. # other may not even have the attributes we are checking.
  678. return ((self._gridspec, self.num1, self.num2)
  679. == (getattr(other, "_gridspec", object()),
  680. getattr(other, "num1", object()),
  681. getattr(other, "num2", object())))
  682. def __hash__(self):
  683. return hash((self._gridspec, self.num1, self.num2))
  684. def subgridspec(self, nrows, ncols, **kwargs):
  685. """
  686. Create a GridSpec within this subplot.
  687. The created `.GridSpecFromSubplotSpec` will have this `SubplotSpec` as
  688. a parent.
  689. Parameters
  690. ----------
  691. nrows : int
  692. Number of rows in grid.
  693. ncols : int
  694. Number or columns in grid.
  695. Returns
  696. -------
  697. `.GridSpecFromSubplotSpec`
  698. Other Parameters
  699. ----------------
  700. **kwargs
  701. All other parameters are passed to `.GridSpecFromSubplotSpec`.
  702. See Also
  703. --------
  704. matplotlib.pyplot.subplots
  705. Examples
  706. --------
  707. Adding three subplots in the space occupied by a single subplot::
  708. fig = plt.figure()
  709. gs0 = fig.add_gridspec(3, 1)
  710. ax1 = fig.add_subplot(gs0[0])
  711. ax2 = fig.add_subplot(gs0[1])
  712. gssub = gs0[2].subgridspec(1, 3)
  713. for i in range(3):
  714. fig.add_subplot(gssub[0, i])
  715. """
  716. return GridSpecFromSubplotSpec(nrows, ncols, self, **kwargs)