bokeh_renderer.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. from __future__ import annotations
  2. import io
  3. from typing import TYPE_CHECKING, Any
  4. from bokeh.io import export_png, export_svg, show
  5. from bokeh.io.export import get_screenshot_as_png
  6. from bokeh.layouts import gridplot
  7. from bokeh.models.annotations.labels import Label
  8. from bokeh.palettes import Category10
  9. from bokeh.plotting import figure
  10. import numpy as np
  11. from contourpy import FillType, LineType
  12. from contourpy.util.bokeh_util import filled_to_bokeh, lines_to_bokeh
  13. from contourpy.util.renderer import Renderer
  14. if TYPE_CHECKING:
  15. from bokeh.models import GridPlot
  16. from bokeh.palettes import Palette
  17. from numpy.typing import ArrayLike
  18. from selenium.webdriver.remote.webdriver import WebDriver
  19. from contourpy._contourpy import FillReturn, LineReturn
  20. class BokehRenderer(Renderer):
  21. _figures: list[figure]
  22. _layout: GridPlot
  23. _palette: Palette
  24. _want_svg: bool
  25. """Utility renderer using Bokeh to render a grid of plots over the same (x, y) range.
  26. Args:
  27. nrows (int, optional): Number of rows of plots, default ``1``.
  28. ncols (int, optional): Number of columns of plots, default ``1``.
  29. figsize (tuple(float, float), optional): Figure size in inches (assuming 100 dpi), default
  30. ``(9, 9)``.
  31. show_frame (bool, optional): Whether to show frame and axes ticks, default ``True``.
  32. want_svg (bool, optional): Whether output is required in SVG format or not, default
  33. ``False``.
  34. Warning:
  35. :class:`~contourpy.util.bokeh_renderer.BokehRenderer`, unlike
  36. :class:`~contourpy.util.mpl_renderer.MplRenderer`, needs to be told in advance if output to
  37. SVG format will be required later, otherwise it will assume PNG output.
  38. """
  39. def __init__(
  40. self,
  41. nrows: int = 1,
  42. ncols: int = 1,
  43. figsize: tuple[float, float] = (9, 9),
  44. show_frame: bool = True,
  45. want_svg: bool = False,
  46. ) -> None:
  47. self._want_svg = want_svg
  48. self._palette = Category10[10]
  49. total_size = 100*np.asarray(figsize, dtype=int) # Assuming 100 dpi.
  50. nfigures = nrows*ncols
  51. self._figures = []
  52. backend = "svg" if self._want_svg else "canvas"
  53. for _ in range(nfigures):
  54. fig = figure(output_backend=backend)
  55. fig.xgrid.visible = False
  56. fig.ygrid.visible = False
  57. self._figures.append(fig)
  58. if not show_frame:
  59. fig.outline_line_color = None # type: ignore[assignment]
  60. fig.axis.visible = False
  61. self._layout = gridplot(
  62. self._figures, ncols=ncols, toolbar_location=None, # type: ignore[arg-type]
  63. width=total_size[0] // ncols, height=total_size[1] // nrows)
  64. def _convert_color(self, color: str) -> str:
  65. if isinstance(color, str) and color[0] == "C":
  66. index = int(color[1:])
  67. color = self._palette[index]
  68. return color
  69. def _get_figure(self, ax: figure | int) -> figure:
  70. if isinstance(ax, int):
  71. ax = self._figures[ax]
  72. return ax
  73. def filled(
  74. self,
  75. filled: FillReturn,
  76. fill_type: FillType,
  77. ax: figure | int = 0,
  78. color: str = "C0",
  79. alpha: float = 0.7,
  80. ) -> None:
  81. """Plot filled contours on a single plot.
  82. Args:
  83. filled (sequence of arrays): Filled contour data as returned by
  84. :func:`~contourpy.ContourGenerator.filled`.
  85. fill_type (FillType): Type of ``filled`` data, as returned by
  86. :attr:`~contourpy.ContourGenerator.fill_type`.
  87. ax (int or Bokeh Figure, optional): Which plot to use, default ``0``.
  88. color (str, optional): Color to plot with. May be a string color or the letter ``"C"``
  89. followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
  90. ``Category10`` palette. Default ``"C0"``.
  91. alpha (float, optional): Opacity to plot with, default ``0.7``.
  92. """
  93. fig = self._get_figure(ax)
  94. color = self._convert_color(color)
  95. xs, ys = filled_to_bokeh(filled, fill_type)
  96. if len(xs) > 0:
  97. fig.multi_polygons(xs=[xs], ys=[ys], color=color, fill_alpha=alpha, line_width=0)
  98. def grid(
  99. self,
  100. x: ArrayLike,
  101. y: ArrayLike,
  102. ax: figure | int = 0,
  103. color: str = "black",
  104. alpha: float = 0.1,
  105. point_color: str | None = None,
  106. quad_as_tri_alpha: float = 0,
  107. ) -> None:
  108. """Plot quad grid lines on a single plot.
  109. Args:
  110. x (array-like of shape (ny, nx) or (nx,)): The x-coordinates of the grid points.
  111. y (array-like of shape (ny, nx) or (ny,)): The y-coordinates of the grid points.
  112. ax (int or Bokeh Figure, optional): Which plot to use, default ``0``.
  113. color (str, optional): Color to plot grid lines, default ``"black"``.
  114. alpha (float, optional): Opacity to plot lines with, default ``0.1``.
  115. point_color (str, optional): Color to plot grid points or ``None`` if grid points
  116. should not be plotted, default ``None``.
  117. quad_as_tri_alpha (float, optional): Opacity to plot ``quad_as_tri`` grid, default
  118. ``0``.
  119. Colors may be a string color or the letter ``"C"`` followed by an integer in the range
  120. ``"C0"`` to ``"C9"`` to use a color from the ``Category10`` palette.
  121. Warning:
  122. ``quad_as_tri_alpha > 0`` plots all quads as though they are unmasked.
  123. """
  124. fig = self._get_figure(ax)
  125. x, y = self._grid_as_2d(x, y)
  126. xs = [row for row in x] + [row for row in x.T]
  127. ys = [row for row in y] + [row for row in y.T]
  128. kwargs = dict(line_color=color, alpha=alpha)
  129. fig.multi_line(xs, ys, **kwargs)
  130. if quad_as_tri_alpha > 0:
  131. # Assumes no quad mask.
  132. xmid = (0.25*(x[:-1, :-1] + x[1:, :-1] + x[:-1, 1:] + x[1:, 1:])).ravel()
  133. ymid = (0.25*(y[:-1, :-1] + y[1:, :-1] + y[:-1, 1:] + y[1:, 1:])).ravel()
  134. fig.multi_line(
  135. [row for row in np.stack((x[:-1, :-1].ravel(), xmid, x[1:, 1:].ravel()), axis=1)],
  136. [row for row in np.stack((y[:-1, :-1].ravel(), ymid, y[1:, 1:].ravel()), axis=1)],
  137. **kwargs)
  138. fig.multi_line(
  139. [row for row in np.stack((x[:-1, 1:].ravel(), xmid, x[1:, :-1].ravel()), axis=1)],
  140. [row for row in np.stack((y[:-1, 1:].ravel(), ymid, y[1:, :-1].ravel()), axis=1)],
  141. **kwargs)
  142. if point_color is not None:
  143. fig.circle(
  144. x=x.ravel(), y=y.ravel(), fill_color=color, line_color=None, alpha=alpha, size=8)
  145. def lines(
  146. self,
  147. lines: LineReturn,
  148. line_type: LineType,
  149. ax: figure | int = 0,
  150. color: str = "C0",
  151. alpha: float = 1.0,
  152. linewidth: float = 1,
  153. ) -> None:
  154. """Plot contour lines on a single plot.
  155. Args:
  156. lines (sequence of arrays): Contour line data as returned by
  157. :func:`~contourpy.ContourGenerator.lines`.
  158. line_type (LineType): Type of ``lines`` data, as returned by
  159. :attr:`~contourpy.ContourGenerator.line_type`.
  160. ax (int or Bokeh Figure, optional): Which plot to use, default ``0``.
  161. color (str, optional): Color to plot lines. May be a string color or the letter ``"C"``
  162. followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
  163. ``Category10`` palette. Default ``"C0"``.
  164. alpha (float, optional): Opacity to plot lines with, default ``1.0``.
  165. linewidth (float, optional): Width of lines, default ``1``.
  166. Note:
  167. Assumes all lines are open line strips not closed line loops.
  168. """
  169. fig = self._get_figure(ax)
  170. color = self._convert_color(color)
  171. xs, ys = lines_to_bokeh(lines, line_type)
  172. if len(xs) > 0:
  173. fig.multi_line(xs, ys, line_color=color, line_alpha=alpha, line_width=linewidth)
  174. def mask(
  175. self,
  176. x: ArrayLike,
  177. y: ArrayLike,
  178. z: ArrayLike | np.ma.MaskedArray[Any, Any],
  179. ax: figure | int = 0,
  180. color: str = "black",
  181. ) -> None:
  182. """Plot masked out grid points as circles on a single plot.
  183. Args:
  184. x (array-like of shape (ny, nx) or (nx,)): The x-coordinates of the grid points.
  185. y (array-like of shape (ny, nx) or (ny,)): The y-coordinates of the grid points.
  186. z (masked array of shape (ny, nx): z-values.
  187. ax (int or Bokeh Figure, optional): Which plot to use, default ``0``.
  188. color (str, optional): Circle color, default ``"black"``.
  189. """
  190. mask = np.ma.getmask(z) # type: ignore[no-untyped-call]
  191. if mask is np.ma.nomask:
  192. return
  193. fig = self._get_figure(ax)
  194. color = self._convert_color(color)
  195. x, y = self._grid_as_2d(x, y)
  196. fig.circle(x[mask], y[mask], fill_color=color, size=10)
  197. def save(
  198. self,
  199. filename: str,
  200. transparent: bool = False,
  201. *,
  202. webdriver: WebDriver | None = None,
  203. ) -> None:
  204. """Save plots to SVG or PNG file.
  205. Args:
  206. filename (str): Filename to save to.
  207. transparent (bool, optional): Whether background should be transparent, default
  208. ``False``.
  209. webdriver (WebDriver, optional): Selenium WebDriver instance to use to create the image.
  210. Warning:
  211. To output to SVG file, ``want_svg=True`` must have been passed to the constructor.
  212. """
  213. if transparent:
  214. for fig in self._figures:
  215. fig.background_fill_color = None # type: ignore[assignment]
  216. fig.border_fill_color = None # type: ignore[assignment]
  217. if self._want_svg:
  218. export_svg(self._layout, filename=filename, webdriver=webdriver)
  219. else:
  220. export_png(self._layout, filename=filename, webdriver=webdriver)
  221. def save_to_buffer(self, *, webdriver: WebDriver | None = None) -> io.BytesIO:
  222. """Save plots to an ``io.BytesIO`` buffer.
  223. Args:
  224. webdriver (WebDriver, optional): Selenium WebDriver instance to use to create the image.
  225. Return:
  226. BytesIO: PNG image buffer.
  227. """
  228. image = get_screenshot_as_png(self._layout, driver=webdriver)
  229. buffer = io.BytesIO()
  230. image.save(buffer, "png")
  231. return buffer
  232. def show(self) -> None:
  233. """Show plots in web browser, in usual Bokeh manner.
  234. """
  235. show(self._layout)
  236. def title(self, title: str, ax: figure | int = 0, color: str | None = None) -> None:
  237. """Set the title of a single plot.
  238. Args:
  239. title (str): Title text.
  240. ax (int or Bokeh Figure, optional): Which plot to set the title of, default ``0``.
  241. color (str, optional): Color to set title. May be a string color or the letter ``"C"``
  242. followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
  243. ``Category10`` palette. Default ``None`` which is ``black``.
  244. """
  245. fig = self._get_figure(ax)
  246. fig.title = title # type: ignore[assignment]
  247. fig.title.align = "center" # type: ignore[attr-defined]
  248. if color is not None:
  249. fig.title.text_color = self._convert_color(color) # type: ignore[attr-defined]
  250. def z_values(
  251. self,
  252. x: ArrayLike,
  253. y: ArrayLike,
  254. z: ArrayLike,
  255. ax: figure | int = 0,
  256. color: str = "green",
  257. fmt: str = ".1f",
  258. quad_as_tri: bool = False,
  259. ) -> None:
  260. """Show ``z`` values on a single plot.
  261. Args:
  262. x (array-like of shape (ny, nx) or (nx,)): The x-coordinates of the grid points.
  263. y (array-like of shape (ny, nx) or (ny,)): The y-coordinates of the grid points.
  264. z (array-like of shape (ny, nx): z-values.
  265. ax (int or Bokeh Figure, optional): Which plot to use, default ``0``.
  266. color (str, optional): Color of added text. May be a string color or the letter ``"C"``
  267. followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
  268. ``Category10`` palette. Default ``"green"``.
  269. fmt (str, optional): Format to display z-values, default ``".1f"``.
  270. quad_as_tri (bool, optional): Whether to show z-values at the ``quad_as_tri`` centres
  271. of quads.
  272. Warning:
  273. ``quad_as_tri=True`` shows z-values for all quads, even if masked.
  274. """
  275. fig = self._get_figure(ax)
  276. color = self._convert_color(color)
  277. x, y = self._grid_as_2d(x, y)
  278. z = np.asarray(z)
  279. ny, nx = z.shape
  280. kwargs = dict(text_color=color, text_align="center", text_baseline="middle")
  281. for j in range(ny):
  282. for i in range(nx):
  283. fig.add_layout(Label(x=x[j, i], y=y[j, i], text=f"{z[j, i]:{fmt}}", **kwargs))
  284. if quad_as_tri:
  285. for j in range(ny-1):
  286. for i in range(nx-1):
  287. xx = np.mean(x[j:j+2, i:i+2])
  288. yy = np.mean(y[j:j+2, i:i+2])
  289. zz = np.mean(z[j:j+2, i:i+2])
  290. fig.add_layout(Label(x=xx, y=yy, text=f"{zz:{fmt}}", **kwargs))