bokeh_renderer.py 14 KB

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