mpl_renderer.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. from __future__ import annotations
  2. import io
  3. from typing import TYPE_CHECKING, Any, cast
  4. import matplotlib.collections as mcollections
  5. import matplotlib.pyplot as plt
  6. import numpy as np
  7. from contourpy import FillType, LineType
  8. from contourpy.util.mpl_util import filled_to_mpl_paths, lines_to_mpl_paths, mpl_codes_to_offsets
  9. from contourpy.util.renderer import Renderer
  10. if TYPE_CHECKING:
  11. from matplotlib.axes import Axes
  12. from matplotlib.figure import Figure
  13. from numpy.typing import ArrayLike
  14. import contourpy._contourpy as cpy
  15. class MplRenderer(Renderer):
  16. _axes: Axes
  17. _fig: Figure
  18. _want_tight: bool
  19. """Utility renderer using Matplotlib to render a grid of plots over the same (x, y) range.
  20. Args:
  21. nrows (int, optional): Number of rows of plots, default ``1``.
  22. ncols (int, optional): Number of columns of plots, default ``1``.
  23. figsize (tuple(float, float), optional): Figure size in inches, default ``(9, 9)``.
  24. show_frame (bool, optional): Whether to show frame and axes ticks, default ``True``.
  25. backend (str, optional): Matplotlib backend to use or ``None`` for default backend.
  26. Default ``None``.
  27. gridspec_kw (dict, optional): Gridspec keyword arguments to pass to ``plt.subplots``,
  28. default None.
  29. """
  30. def __init__(
  31. self,
  32. nrows: int = 1,
  33. ncols: int = 1,
  34. figsize: tuple[float, float] = (9, 9),
  35. show_frame: bool = True,
  36. backend: str | None = None,
  37. gridspec_kw: dict[str, Any] | None = None,
  38. ) -> None:
  39. if backend is not None:
  40. import matplotlib
  41. matplotlib.use(backend)
  42. kwargs = dict(figsize=figsize, squeeze=False, sharex=True, sharey=True)
  43. if gridspec_kw is not None:
  44. kwargs["gridspec_kw"] = gridspec_kw
  45. else:
  46. kwargs["subplot_kw"] = dict(aspect="equal")
  47. self._fig, axes = plt.subplots(nrows, ncols, **kwargs)
  48. self._axes = axes.flatten()
  49. if not show_frame:
  50. for ax in self._axes:
  51. ax.axis("off")
  52. self._want_tight = True
  53. def __del__(self) -> None:
  54. if hasattr(self, "_fig"):
  55. plt.close(self._fig)
  56. def _autoscale(self) -> None:
  57. # Using axes._need_autoscale attribute if need to autoscale before rendering after adding
  58. # lines/filled. Only want to autoscale once per axes regardless of how many lines/filled
  59. # added.
  60. for ax in self._axes:
  61. if getattr(ax, "_need_autoscale", False):
  62. ax.autoscale_view(tight=True)
  63. ax._need_autoscale = False
  64. if self._want_tight and len(self._axes) > 1:
  65. self._fig.tight_layout()
  66. def _get_ax(self, ax: Axes | int) -> Axes:
  67. if isinstance(ax, int):
  68. ax = self._axes[ax]
  69. return ax
  70. def filled(
  71. self,
  72. filled: cpy.FillReturn,
  73. fill_type: FillType,
  74. ax: Axes | int = 0,
  75. color: str = "C0",
  76. alpha: float = 0.7,
  77. ) -> None:
  78. """Plot filled contours on a single Axes.
  79. Args:
  80. filled (sequence of arrays): Filled contour data as returned by
  81. :func:`~contourpy.ContourGenerator.filled`.
  82. fill_type (FillType): Type of ``filled`` data, as returned by
  83. :attr:`~contourpy.ContourGenerator.fill_type`.
  84. ax (int or Maplotlib Axes, optional): Which axes to plot on, default ``0``.
  85. color (str, optional): Color to plot with. May be a string color or the letter ``"C"``
  86. followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
  87. ``tab10`` colormap. Default ``"C0"``.
  88. alpha (float, optional): Opacity to plot with, default ``0.7``.
  89. """
  90. ax = self._get_ax(ax)
  91. paths = filled_to_mpl_paths(filled, fill_type)
  92. collection = mcollections.PathCollection(
  93. paths, facecolors=color, edgecolors="none", lw=0, alpha=alpha)
  94. ax.add_collection(collection)
  95. ax._need_autoscale = True
  96. def grid(
  97. self,
  98. x: ArrayLike,
  99. y: ArrayLike,
  100. ax: Axes | int = 0,
  101. color: str = "black",
  102. alpha: float = 0.1,
  103. point_color: str | None = None,
  104. quad_as_tri_alpha: float = 0,
  105. ) -> None:
  106. """Plot quad grid lines on a single Axes.
  107. Args:
  108. x (array-like of shape (ny, nx) or (nx,)): The x-coordinates of the grid points.
  109. y (array-like of shape (ny, nx) or (ny,)): The y-coordinates of the grid points.
  110. ax (int or Matplotlib Axes, optional): Which Axes to plot on, default ``0``.
  111. color (str, optional): Color to plot grid lines, default ``"black"``.
  112. alpha (float, optional): Opacity to plot lines with, default ``0.1``.
  113. point_color (str, optional): Color to plot grid points or ``None`` if grid points
  114. should not be plotted, default ``None``.
  115. quad_as_tri_alpha (float, optional): Opacity to plot ``quad_as_tri`` grid, default 0.
  116. Colors may be a string color or the letter ``"C"`` followed by an integer in the range
  117. ``"C0"`` to ``"C9"`` to use a color from the ``tab10`` colormap.
  118. Warning:
  119. ``quad_as_tri_alpha > 0`` plots all quads as though they are unmasked.
  120. """
  121. ax = self._get_ax(ax)
  122. x, y = self._grid_as_2d(x, y)
  123. kwargs = dict(color=color, alpha=alpha)
  124. ax.plot(x, y, x.T, y.T, **kwargs)
  125. if quad_as_tri_alpha > 0:
  126. # Assumes no quad mask.
  127. xmid = 0.25*(x[:-1, :-1] + x[1:, :-1] + x[:-1, 1:] + x[1:, 1:])
  128. ymid = 0.25*(y[:-1, :-1] + y[1:, :-1] + y[:-1, 1:] + y[1:, 1:])
  129. kwargs["alpha"] = quad_as_tri_alpha
  130. ax.plot(
  131. np.stack((x[:-1, :-1], xmid, x[1:, 1:])).reshape((3, -1)),
  132. np.stack((y[:-1, :-1], ymid, y[1:, 1:])).reshape((3, -1)),
  133. np.stack((x[1:, :-1], xmid, x[:-1, 1:])).reshape((3, -1)),
  134. np.stack((y[1:, :-1], ymid, y[:-1, 1:])).reshape((3, -1)),
  135. **kwargs)
  136. if point_color is not None:
  137. ax.plot(x, y, color=point_color, alpha=alpha, marker="o", lw=0)
  138. ax._need_autoscale = True
  139. def lines(
  140. self,
  141. lines: cpy.LineReturn,
  142. line_type: LineType,
  143. ax: Axes | int = 0,
  144. color: str = "C0",
  145. alpha: float = 1.0,
  146. linewidth: float = 1,
  147. ) -> None:
  148. """Plot contour lines on a single Axes.
  149. Args:
  150. lines (sequence of arrays): Contour line data as returned by
  151. :func:`~contourpy.ContourGenerator.lines`.
  152. line_type (LineType): Type of ``lines`` data, as returned by
  153. :attr:`~contourpy.ContourGenerator.line_type`.
  154. ax (int or Matplotlib Axes, optional): Which Axes to plot on, default ``0``.
  155. color (str, optional): Color to plot lines. May be a string color or the letter ``"C"``
  156. followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
  157. ``tab10`` colormap. Default ``"C0"``.
  158. alpha (float, optional): Opacity to plot lines with, default ``1.0``.
  159. linewidth (float, optional): Width of lines, default ``1``.
  160. """
  161. ax = self._get_ax(ax)
  162. paths = lines_to_mpl_paths(lines, line_type)
  163. collection = mcollections.PathCollection(
  164. paths, facecolors="none", edgecolors=color, lw=linewidth, alpha=alpha)
  165. ax.add_collection(collection)
  166. ax._need_autoscale = True
  167. def mask(
  168. self,
  169. x: ArrayLike,
  170. y: ArrayLike,
  171. z: ArrayLike | np.ma.MaskedArray[Any, Any],
  172. ax: Axes | int = 0,
  173. color: str = "black",
  174. ) -> None:
  175. """Plot masked out grid points as circles on a single Axes.
  176. Args:
  177. x (array-like of shape (ny, nx) or (nx,)): The x-coordinates of the grid points.
  178. y (array-like of shape (ny, nx) or (ny,)): The y-coordinates of the grid points.
  179. z (masked array of shape (ny, nx): z-values.
  180. ax (int or Matplotlib Axes, optional): Which Axes to plot on, default ``0``.
  181. color (str, optional): Circle color, default ``"black"``.
  182. """
  183. mask = np.ma.getmask(z) # type: ignore[no-untyped-call]
  184. if mask is np.ma.nomask:
  185. return
  186. ax = self._get_ax(ax)
  187. x, y = self._grid_as_2d(x, y)
  188. ax.plot(x[mask], y[mask], "o", c=color)
  189. def save(self, filename: str, transparent: bool = False) -> None:
  190. """Save plots to SVG or PNG file.
  191. Args:
  192. filename (str): Filename to save to.
  193. transparent (bool, optional): Whether background should be transparent, default
  194. ``False``.
  195. """
  196. self._autoscale()
  197. self._fig.savefig(filename, transparent=transparent)
  198. def save_to_buffer(self) -> io.BytesIO:
  199. """Save plots to an ``io.BytesIO`` buffer.
  200. Return:
  201. BytesIO: PNG image buffer.
  202. """
  203. self._autoscale()
  204. buf = io.BytesIO()
  205. self._fig.savefig(buf, format="png")
  206. buf.seek(0)
  207. return buf
  208. def show(self) -> None:
  209. """Show plots in an interactive window, in the usual Matplotlib manner.
  210. """
  211. self._autoscale()
  212. plt.show()
  213. def title(self, title: str, ax: Axes | int = 0, color: str | None = None) -> None:
  214. """Set the title of a single Axes.
  215. Args:
  216. title (str): Title text.
  217. ax (int or Matplotlib Axes, optional): Which Axes to set the title of, default ``0``.
  218. color (str, optional): Color to set title. May be a string color or the letter ``"C"``
  219. followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
  220. ``tab10`` colormap. Default is ``None`` which uses Matplotlib's default title color
  221. that depends on the stylesheet in use.
  222. """
  223. if color:
  224. self._get_ax(ax).set_title(title, color=color)
  225. else:
  226. self._get_ax(ax).set_title(title)
  227. def z_values(
  228. self,
  229. x: ArrayLike,
  230. y: ArrayLike,
  231. z: ArrayLike,
  232. ax: Axes | int = 0,
  233. color: str = "green",
  234. fmt: str = ".1f",
  235. quad_as_tri: bool = False,
  236. ) -> None:
  237. """Show ``z`` values on a single Axes.
  238. Args:
  239. x (array-like of shape (ny, nx) or (nx,)): The x-coordinates of the grid points.
  240. y (array-like of shape (ny, nx) or (ny,)): The y-coordinates of the grid points.
  241. z (array-like of shape (ny, nx): z-values.
  242. ax (int or Matplotlib Axes, optional): Which Axes to plot on, default ``0``.
  243. color (str, optional): Color of added text. May be a string color or the letter ``"C"``
  244. followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
  245. ``tab10`` colormap. Default ``"green"``.
  246. fmt (str, optional): Format to display z-values, default ``".1f"``.
  247. quad_as_tri (bool, optional): Whether to show z-values at the ``quad_as_tri`` centers
  248. of quads.
  249. Warning:
  250. ``quad_as_tri=True`` shows z-values for all quads, even if masked.
  251. """
  252. ax = self._get_ax(ax)
  253. x, y = self._grid_as_2d(x, y)
  254. z = np.asarray(z)
  255. ny, nx = z.shape
  256. for j in range(ny):
  257. for i in range(nx):
  258. ax.text(x[j, i], y[j, i], f"{z[j, i]:{fmt}}", ha="center", va="center",
  259. color=color, clip_on=True)
  260. if quad_as_tri:
  261. for j in range(ny-1):
  262. for i in range(nx-1):
  263. xx = np.mean(x[j:j+2, i:i+2])
  264. yy = np.mean(y[j:j+2, i:i+2])
  265. zz = np.mean(z[j:j+2, i:i+2])
  266. ax.text(xx, yy, f"{zz:{fmt}}", ha="center", va="center", color=color,
  267. clip_on=True)
  268. class MplTestRenderer(MplRenderer):
  269. """Test renderer implemented using Matplotlib.
  270. No whitespace around plots and no spines/ticks displayed.
  271. Uses Agg backend, so can only save to file/buffer, cannot call ``show()``.
  272. """
  273. def __init__(
  274. self,
  275. nrows: int = 1,
  276. ncols: int = 1,
  277. figsize: tuple[float, float] = (9, 9),
  278. ) -> None:
  279. gridspec = {
  280. "left": 0.01,
  281. "right": 0.99,
  282. "top": 0.99,
  283. "bottom": 0.01,
  284. "wspace": 0.01,
  285. "hspace": 0.01,
  286. }
  287. super().__init__(
  288. nrows, ncols, figsize, show_frame=True, backend="Agg", gridspec_kw=gridspec,
  289. )
  290. for ax in self._axes:
  291. ax.set_xmargin(0.0)
  292. ax.set_ymargin(0.0)
  293. ax.set_xticks([])
  294. ax.set_yticks([])
  295. self._want_tight = False
  296. class MplDebugRenderer(MplRenderer):
  297. """Debug renderer implemented using Matplotlib.
  298. Extends ``MplRenderer`` to add extra information to help in debugging such as markers, arrows,
  299. text, etc.
  300. """
  301. def __init__(
  302. self,
  303. nrows: int = 1,
  304. ncols: int = 1,
  305. figsize: tuple[float, float] = (9, 9),
  306. show_frame: bool = True,
  307. ) -> None:
  308. super().__init__(nrows, ncols, figsize, show_frame)
  309. def _arrow(
  310. self,
  311. ax: Axes,
  312. line_start: cpy.CoordinateArray,
  313. line_end: cpy.CoordinateArray,
  314. color: str,
  315. alpha: float,
  316. arrow_size: float,
  317. ) -> None:
  318. mid = 0.5*(line_start + line_end)
  319. along = line_end - line_start
  320. along /= np.sqrt(np.dot(along, along)) # Unit vector.
  321. right = np.asarray((along[1], -along[0]))
  322. arrow = np.stack((
  323. mid - (along*0.5 - right)*arrow_size,
  324. mid + along*0.5*arrow_size,
  325. mid - (along*0.5 + right)*arrow_size,
  326. ))
  327. ax.plot(arrow[:, 0], arrow[:, 1], "-", c=color, alpha=alpha)
  328. def _filled_to_lists_of_points_and_offsets(
  329. self,
  330. filled: cpy.FillReturn,
  331. fill_type: FillType,
  332. ) -> tuple[list[cpy.PointArray], list[cpy.OffsetArray]]:
  333. if fill_type == FillType.OuterCode:
  334. if TYPE_CHECKING:
  335. filled = cast(cpy.FillReturn_OuterCode, filled)
  336. all_points = filled[0]
  337. all_offsets = [mpl_codes_to_offsets(codes) for codes in filled[1]]
  338. elif fill_type == FillType.ChunkCombinedCode:
  339. if TYPE_CHECKING:
  340. filled = cast(cpy.FillReturn_ChunkCombinedCode, filled)
  341. all_points = [points for points in filled[0] if points is not None]
  342. all_offsets = [mpl_codes_to_offsets(codes) for codes in filled[1] if codes is not None]
  343. elif fill_type == FillType.OuterOffset:
  344. if TYPE_CHECKING:
  345. filled = cast(cpy.FillReturn_OuterOffset, filled)
  346. all_points = filled[0]
  347. all_offsets = filled[1]
  348. elif fill_type == FillType.ChunkCombinedOffset:
  349. if TYPE_CHECKING:
  350. filled = cast(cpy.FillReturn_ChunkCombinedOffset, filled)
  351. all_points = [points for points in filled[0] if points is not None]
  352. all_offsets = [offsets for offsets in filled[1] if offsets is not None]
  353. elif fill_type == FillType.ChunkCombinedCodeOffset:
  354. if TYPE_CHECKING:
  355. filled = cast(cpy.FillReturn_ChunkCombinedCodeOffset, filled)
  356. all_points = []
  357. all_offsets = []
  358. for points, codes, outer_offsets in zip(*filled):
  359. if points is None:
  360. continue
  361. if TYPE_CHECKING:
  362. assert codes is not None and outer_offsets is not None
  363. all_points += np.split(points, outer_offsets[1:-1])
  364. all_codes = np.split(codes, outer_offsets[1:-1])
  365. all_offsets += [mpl_codes_to_offsets(codes) for codes in all_codes]
  366. elif fill_type == FillType.ChunkCombinedOffsetOffset:
  367. if TYPE_CHECKING:
  368. filled = cast(cpy.FillReturn_ChunkCombinedOffsetOffset, filled)
  369. all_points = []
  370. all_offsets = []
  371. for points, offsets, outer_offsets in zip(*filled):
  372. if points is None:
  373. continue
  374. if TYPE_CHECKING:
  375. assert offsets is not None and outer_offsets is not None
  376. for i in range(len(outer_offsets)-1):
  377. offs = offsets[outer_offsets[i]:outer_offsets[i+1]+1]
  378. all_points.append(points[offs[0]:offs[-1]])
  379. all_offsets.append(offs - offs[0])
  380. else:
  381. raise RuntimeError(f"Rendering FillType {fill_type} not implemented")
  382. return all_points, all_offsets
  383. def _lines_to_list_of_points(
  384. self, lines: cpy.LineReturn, line_type: LineType,
  385. ) -> list[cpy.PointArray]:
  386. if line_type == LineType.Separate:
  387. if TYPE_CHECKING:
  388. lines = cast(cpy.LineReturn_Separate, lines)
  389. all_lines = lines
  390. elif line_type == LineType.SeparateCode:
  391. if TYPE_CHECKING:
  392. lines = cast(cpy.LineReturn_SeparateCode, lines)
  393. all_lines = lines[0]
  394. elif line_type == LineType.ChunkCombinedCode:
  395. if TYPE_CHECKING:
  396. lines = cast(cpy.LineReturn_ChunkCombinedCode, lines)
  397. all_lines = []
  398. for points, codes in zip(*lines):
  399. if points is not None:
  400. if TYPE_CHECKING:
  401. assert codes is not None
  402. offsets = mpl_codes_to_offsets(codes)
  403. for i in range(len(offsets)-1):
  404. all_lines.append(points[offsets[i]:offsets[i+1]])
  405. elif line_type == LineType.ChunkCombinedOffset:
  406. if TYPE_CHECKING:
  407. lines = cast(cpy.LineReturn_ChunkCombinedOffset, lines)
  408. all_lines = []
  409. for points, all_offsets in zip(*lines):
  410. if points is not None:
  411. if TYPE_CHECKING:
  412. assert all_offsets is not None
  413. for i in range(len(all_offsets)-1):
  414. all_lines.append(points[all_offsets[i]:all_offsets[i+1]])
  415. else:
  416. raise RuntimeError(f"Rendering LineType {line_type} not implemented")
  417. return all_lines
  418. def filled(
  419. self,
  420. filled: cpy.FillReturn,
  421. fill_type: FillType,
  422. ax: Axes | int = 0,
  423. color: str = "C1",
  424. alpha: float = 0.7,
  425. line_color: str = "C0",
  426. line_alpha: float = 0.7,
  427. point_color: str = "C0",
  428. start_point_color: str = "red",
  429. arrow_size: float = 0.1,
  430. ) -> None:
  431. super().filled(filled, fill_type, ax, color, alpha)
  432. if line_color is None and point_color is None:
  433. return
  434. ax = self._get_ax(ax)
  435. all_points, all_offsets = self._filled_to_lists_of_points_and_offsets(filled, fill_type)
  436. # Lines.
  437. if line_color is not None:
  438. for points, offsets in zip(all_points, all_offsets):
  439. for start, end in zip(offsets[:-1], offsets[1:]):
  440. xys = points[start:end]
  441. ax.plot(xys[:, 0], xys[:, 1], c=line_color, alpha=line_alpha)
  442. if arrow_size > 0.0:
  443. n = len(xys)
  444. for i in range(n-1):
  445. self._arrow(ax, xys[i], xys[i+1], line_color, line_alpha, arrow_size)
  446. # Points.
  447. if point_color is not None:
  448. for points, offsets in zip(all_points, all_offsets):
  449. mask = np.ones(offsets[-1], dtype=bool)
  450. mask[offsets[1:]-1] = False # Exclude end points.
  451. if start_point_color is not None:
  452. start_indices = offsets[:-1]
  453. mask[start_indices] = False # Exclude start points.
  454. ax.plot(
  455. points[:, 0][mask], points[:, 1][mask], "o", c=point_color, alpha=line_alpha)
  456. if start_point_color is not None:
  457. ax.plot(points[:, 0][start_indices], points[:, 1][start_indices], "o",
  458. c=start_point_color, alpha=line_alpha)
  459. def lines(
  460. self,
  461. lines: cpy.LineReturn,
  462. line_type: LineType,
  463. ax: Axes | int = 0,
  464. color: str = "C0",
  465. alpha: float = 1.0,
  466. linewidth: float = 1,
  467. point_color: str = "C0",
  468. start_point_color: str = "red",
  469. arrow_size: float = 0.1,
  470. ) -> None:
  471. super().lines(lines, line_type, ax, color, alpha, linewidth)
  472. if arrow_size == 0.0 and point_color is None:
  473. return
  474. ax = self._get_ax(ax)
  475. all_lines = self._lines_to_list_of_points(lines, line_type)
  476. if arrow_size > 0.0:
  477. for line in all_lines:
  478. for i in range(len(line)-1):
  479. self._arrow(ax, line[i], line[i+1], color, alpha, arrow_size)
  480. if point_color is not None:
  481. for line in all_lines:
  482. start_index = 0
  483. end_index = len(line)
  484. if start_point_color is not None:
  485. ax.plot(line[0, 0], line[0, 1], "o", c=start_point_color, alpha=alpha)
  486. start_index = 1
  487. if line[0][0] == line[-1][0] and line[0][1] == line[-1][1]:
  488. end_index -= 1
  489. ax.plot(line[start_index:end_index, 0], line[start_index:end_index, 1], "o",
  490. c=color, alpha=alpha)
  491. def point_numbers(
  492. self,
  493. x: ArrayLike,
  494. y: ArrayLike,
  495. z: ArrayLike,
  496. ax: Axes | int = 0,
  497. color: str = "red",
  498. ) -> None:
  499. ax = self._get_ax(ax)
  500. x, y = self._grid_as_2d(x, y)
  501. z = np.asarray(z)
  502. ny, nx = z.shape
  503. for j in range(ny):
  504. for i in range(nx):
  505. quad = i + j*nx
  506. ax.text(x[j, i], y[j, i], str(quad), ha="right", va="top", color=color,
  507. clip_on=True)
  508. def quad_numbers(
  509. self,
  510. x: ArrayLike,
  511. y: ArrayLike,
  512. z: ArrayLike,
  513. ax: Axes | int = 0,
  514. color: str = "blue",
  515. ) -> None:
  516. ax = self._get_ax(ax)
  517. x, y = self._grid_as_2d(x, y)
  518. z = np.asarray(z)
  519. ny, nx = z.shape
  520. for j in range(1, ny):
  521. for i in range(1, nx):
  522. quad = i + j*nx
  523. xmid = x[j-1:j+1, i-1:i+1].mean()
  524. ymid = y[j-1:j+1, i-1:i+1].mean()
  525. ax.text(xmid, ymid, str(quad), ha="center", va="center", color=color, clip_on=True)
  526. def z_levels(
  527. self,
  528. x: ArrayLike,
  529. y: ArrayLike,
  530. z: ArrayLike,
  531. lower_level: float,
  532. upper_level: float | None = None,
  533. ax: Axes | int = 0,
  534. color: str = "green",
  535. ) -> None:
  536. ax = self._get_ax(ax)
  537. x, y = self._grid_as_2d(x, y)
  538. z = np.asarray(z)
  539. ny, nx = z.shape
  540. for j in range(ny):
  541. for i in range(nx):
  542. zz = z[j, i]
  543. if upper_level is not None and zz > upper_level:
  544. z_level = 2
  545. elif zz > lower_level:
  546. z_level = 1
  547. else:
  548. z_level = 0
  549. ax.text(x[j, i], y[j, i], z_level, ha="left", va="bottom", color=color,
  550. clip_on=True)