Spaces:
Running
Running
from __future__ import annotations | |
import io | |
from typing import TYPE_CHECKING, Any | |
from bokeh.io import export_png, export_svg, show | |
from bokeh.io.export import get_screenshot_as_png | |
from bokeh.layouts import gridplot | |
from bokeh.models.annotations.labels import Label | |
from bokeh.palettes import Category10 | |
from bokeh.plotting import figure | |
import numpy as np | |
from contourpy.enum_util import as_fill_type, as_line_type | |
from contourpy.util.bokeh_util import filled_to_bokeh, lines_to_bokeh | |
from contourpy.util.renderer import Renderer | |
if TYPE_CHECKING: | |
from bokeh.models import GridPlot | |
from bokeh.palettes import Palette | |
from numpy.typing import ArrayLike | |
from selenium.webdriver.remote.webdriver import WebDriver | |
from contourpy import FillType, LineType | |
from contourpy._contourpy import FillReturn, LineReturn | |
class BokehRenderer(Renderer): | |
"""Utility renderer using Bokeh to render a grid of plots over the same (x, y) range. | |
Args: | |
nrows (int, optional): Number of rows of plots, default ``1``. | |
ncols (int, optional): Number of columns of plots, default ``1``. | |
figsize (tuple(float, float), optional): Figure size in inches (assuming 100 dpi), default | |
``(9, 9)``. | |
show_frame (bool, optional): Whether to show frame and axes ticks, default ``True``. | |
want_svg (bool, optional): Whether output is required in SVG format or not, default | |
``False``. | |
Warning: | |
:class:`~.BokehRenderer`, unlike :class:`~.MplRenderer`, needs to be told in advance if | |
output to SVG format will be required later, otherwise it will assume PNG output. | |
""" | |
_figures: list[figure] | |
_layout: GridPlot | |
_palette: Palette | |
_want_svg: bool | |
def __init__( | |
self, | |
nrows: int = 1, | |
ncols: int = 1, | |
figsize: tuple[float, float] = (9, 9), | |
show_frame: bool = True, | |
want_svg: bool = False, | |
) -> None: | |
self._want_svg = want_svg | |
self._palette = Category10[10] | |
total_size = 100*np.asarray(figsize, dtype=int) # Assuming 100 dpi. | |
nfigures = nrows*ncols | |
self._figures = [] | |
backend = "svg" if self._want_svg else "canvas" | |
for _ in range(nfigures): | |
fig = figure(output_backend=backend) | |
fig.xgrid.visible = False | |
fig.ygrid.visible = False | |
self._figures.append(fig) | |
if not show_frame: | |
fig.outline_line_color = None # type: ignore[assignment] | |
fig.axis.visible = False | |
self._layout = gridplot( | |
self._figures, ncols=ncols, toolbar_location=None, # type: ignore[arg-type] | |
width=total_size[0] // ncols, height=total_size[1] // nrows) | |
def _convert_color(self, color: str) -> str: | |
if isinstance(color, str) and color[0] == "C": | |
index = int(color[1:]) | |
color = self._palette[index] | |
return color | |
def _get_figure(self, ax: figure | int) -> figure: | |
if isinstance(ax, int): | |
ax = self._figures[ax] | |
return ax | |
def filled( | |
self, | |
filled: FillReturn, | |
fill_type: FillType | str, | |
ax: figure | int = 0, | |
color: str = "C0", | |
alpha: float = 0.7, | |
) -> None: | |
"""Plot filled contours on a single plot. | |
Args: | |
filled (sequence of arrays): Filled contour data as returned by | |
:meth:`~.ContourGenerator.filled`. | |
fill_type (FillType or str): Type of :meth:`~.ContourGenerator.filled` data as returned | |
by :attr:`~.ContourGenerator.fill_type`, or a string equivalent. | |
ax (int or Bokeh Figure, optional): Which plot to use, default ``0``. | |
color (str, optional): Color to plot with. May be a string color or the letter ``"C"`` | |
followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the | |
``Category10`` palette. Default ``"C0"``. | |
alpha (float, optional): Opacity to plot with, default ``0.7``. | |
""" | |
fill_type = as_fill_type(fill_type) | |
fig = self._get_figure(ax) | |
color = self._convert_color(color) | |
xs, ys = filled_to_bokeh(filled, fill_type) | |
if len(xs) > 0: | |
fig.multi_polygons(xs=[xs], ys=[ys], color=color, fill_alpha=alpha, line_width=0) | |
def grid( | |
self, | |
x: ArrayLike, | |
y: ArrayLike, | |
ax: figure | int = 0, | |
color: str = "black", | |
alpha: float = 0.1, | |
point_color: str | None = None, | |
quad_as_tri_alpha: float = 0, | |
) -> None: | |
"""Plot quad grid lines on a single plot. | |
Args: | |
x (array-like of shape (ny, nx) or (nx,)): The x-coordinates of the grid points. | |
y (array-like of shape (ny, nx) or (ny,)): The y-coordinates of the grid points. | |
ax (int or Bokeh Figure, optional): Which plot to use, default ``0``. | |
color (str, optional): Color to plot grid lines, default ``"black"``. | |
alpha (float, optional): Opacity to plot lines with, default ``0.1``. | |
point_color (str, optional): Color to plot grid points or ``None`` if grid points | |
should not be plotted, default ``None``. | |
quad_as_tri_alpha (float, optional): Opacity to plot ``quad_as_tri`` grid, default | |
``0``. | |
Colors may be a string color or the letter ``"C"`` followed by an integer in the range | |
``"C0"`` to ``"C9"`` to use a color from the ``Category10`` palette. | |
Warning: | |
``quad_as_tri_alpha > 0`` plots all quads as though they are unmasked. | |
""" | |
fig = self._get_figure(ax) | |
x, y = self._grid_as_2d(x, y) | |
xs = list(x) + list(x.T) | |
ys = list(y) + list(y.T) | |
kwargs = {"line_color": color, "alpha": alpha} | |
fig.multi_line(xs, ys, **kwargs) | |
if quad_as_tri_alpha > 0: | |
# Assumes no quad mask. | |
xmid = (0.25*(x[:-1, :-1] + x[1:, :-1] + x[:-1, 1:] + x[1:, 1:])).ravel() | |
ymid = (0.25*(y[:-1, :-1] + y[1:, :-1] + y[:-1, 1:] + y[1:, 1:])).ravel() | |
fig.multi_line( | |
list(np.stack((x[:-1, :-1].ravel(), xmid, x[1:, 1:].ravel()), axis=1)), | |
list(np.stack((y[:-1, :-1].ravel(), ymid, y[1:, 1:].ravel()), axis=1)), | |
**kwargs) | |
fig.multi_line( | |
list(np.stack((x[:-1, 1:].ravel(), xmid, x[1:, :-1].ravel()), axis=1)), | |
list(np.stack((y[:-1, 1:].ravel(), ymid, y[1:, :-1].ravel()), axis=1)), | |
**kwargs) | |
if point_color is not None: | |
fig.circle( | |
x=x.ravel(), y=y.ravel(), fill_color=color, line_color=None, alpha=alpha, size=8) | |
def lines( | |
self, | |
lines: LineReturn, | |
line_type: LineType | str, | |
ax: figure | int = 0, | |
color: str = "C0", | |
alpha: float = 1.0, | |
linewidth: float = 1, | |
) -> None: | |
"""Plot contour lines on a single plot. | |
Args: | |
lines (sequence of arrays): Contour line data as returned by | |
:meth:`~.ContourGenerator.lines`. | |
line_type (LineType or str): Type of :meth:`~.ContourGenerator.lines` data as returned | |
by :attr:`~.ContourGenerator.line_type`, or a string equivalent. | |
ax (int or Bokeh Figure, optional): Which plot to use, default ``0``. | |
color (str, optional): Color to plot lines. May be a string color or the letter ``"C"`` | |
followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the | |
``Category10`` palette. Default ``"C0"``. | |
alpha (float, optional): Opacity to plot lines with, default ``1.0``. | |
linewidth (float, optional): Width of lines, default ``1``. | |
Note: | |
Assumes all lines are open line strips not closed line loops. | |
""" | |
line_type = as_line_type(line_type) | |
fig = self._get_figure(ax) | |
color = self._convert_color(color) | |
xs, ys = lines_to_bokeh(lines, line_type) | |
if xs is not None: | |
fig.line(xs, ys, line_color=color, line_alpha=alpha, line_width=linewidth) | |
def mask( | |
self, | |
x: ArrayLike, | |
y: ArrayLike, | |
z: ArrayLike | np.ma.MaskedArray[Any, Any], | |
ax: figure | int = 0, | |
color: str = "black", | |
) -> None: | |
"""Plot masked out grid points as circles on a single plot. | |
Args: | |
x (array-like of shape (ny, nx) or (nx,)): The x-coordinates of the grid points. | |
y (array-like of shape (ny, nx) or (ny,)): The y-coordinates of the grid points. | |
z (masked array of shape (ny, nx): z-values. | |
ax (int or Bokeh Figure, optional): Which plot to use, default ``0``. | |
color (str, optional): Circle color, default ``"black"``. | |
""" | |
mask = np.ma.getmask(z) # type: ignore[no-untyped-call] | |
if mask is np.ma.nomask: | |
return | |
fig = self._get_figure(ax) | |
color = self._convert_color(color) | |
x, y = self._grid_as_2d(x, y) | |
fig.circle(x[mask], y[mask], fill_color=color, size=10) | |
def save( | |
self, | |
filename: str, | |
transparent: bool = False, | |
*, | |
webdriver: WebDriver | None = None, | |
) -> None: | |
"""Save plots to SVG or PNG file. | |
Args: | |
filename (str): Filename to save to. | |
transparent (bool, optional): Whether background should be transparent, default | |
``False``. | |
webdriver (WebDriver, optional): Selenium WebDriver instance to use to create the image. | |
.. versionadded:: 1.1.1 | |
Warning: | |
To output to SVG file, ``want_svg=True`` must have been passed to the constructor. | |
""" | |
if transparent: | |
for fig in self._figures: | |
fig.background_fill_color = None # type: ignore[assignment] | |
fig.border_fill_color = None # type: ignore[assignment] | |
if self._want_svg: | |
export_svg(self._layout, filename=filename, webdriver=webdriver) | |
else: | |
export_png(self._layout, filename=filename, webdriver=webdriver) | |
def save_to_buffer(self, *, webdriver: WebDriver | None = None) -> io.BytesIO: | |
"""Save plots to an ``io.BytesIO`` buffer. | |
Args: | |
webdriver (WebDriver, optional): Selenium WebDriver instance to use to create the image. | |
.. versionadded:: 1.1.1 | |
Return: | |
BytesIO: PNG image buffer. | |
""" | |
image = get_screenshot_as_png(self._layout, driver=webdriver) | |
buffer = io.BytesIO() | |
image.save(buffer, "png") | |
return buffer | |
def show(self) -> None: | |
"""Show plots in web browser, in usual Bokeh manner. | |
""" | |
show(self._layout) | |
def title(self, title: str, ax: figure | int = 0, color: str | None = None) -> None: | |
"""Set the title of a single plot. | |
Args: | |
title (str): Title text. | |
ax (int or Bokeh Figure, optional): Which plot to set the title of, default ``0``. | |
color (str, optional): Color to set title. May be a string color or the letter ``"C"`` | |
followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the | |
``Category10`` palette. Default ``None`` which is ``black``. | |
""" | |
fig = self._get_figure(ax) | |
fig.title = title # type: ignore[assignment] | |
fig.title.align = "center" # type: ignore[attr-defined] | |
if color is not None: | |
fig.title.text_color = self._convert_color(color) # type: ignore[attr-defined] | |
def z_values( | |
self, | |
x: ArrayLike, | |
y: ArrayLike, | |
z: ArrayLike, | |
ax: figure | int = 0, | |
color: str = "green", | |
fmt: str = ".1f", | |
quad_as_tri: bool = False, | |
) -> None: | |
"""Show ``z`` values on a single plot. | |
Args: | |
x (array-like of shape (ny, nx) or (nx,)): The x-coordinates of the grid points. | |
y (array-like of shape (ny, nx) or (ny,)): The y-coordinates of the grid points. | |
z (array-like of shape (ny, nx): z-values. | |
ax (int or Bokeh Figure, optional): Which plot to use, default ``0``. | |
color (str, optional): Color of added text. May be a string color or the letter ``"C"`` | |
followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the | |
``Category10`` palette. Default ``"green"``. | |
fmt (str, optional): Format to display z-values, default ``".1f"``. | |
quad_as_tri (bool, optional): Whether to show z-values at the ``quad_as_tri`` centres | |
of quads. | |
Warning: | |
``quad_as_tri=True`` shows z-values for all quads, even if masked. | |
""" | |
fig = self._get_figure(ax) | |
color = self._convert_color(color) | |
x, y = self._grid_as_2d(x, y) | |
z = np.asarray(z) | |
ny, nx = z.shape | |
kwargs = {"text_color": color, "text_align": "center", "text_baseline": "middle"} | |
for j in range(ny): | |
for i in range(nx): | |
fig.add_layout(Label(x=x[j, i], y=y[j, i], text=f"{z[j, i]:{fmt}}", **kwargs)) | |
if quad_as_tri: | |
for j in range(ny-1): | |
for i in range(nx-1): | |
xx = np.mean(x[j:j+2, i:i+2]) | |
yy = np.mean(y[j:j+2, i:i+2]) | |
zz = np.mean(z[j:j+2, i:i+2]) | |
fig.add_layout(Label(x=xx, y=yy, text=f"{zz:{fmt}}", **kwargs)) | |