I am done

This commit is contained in:
2024-10-30 22:14:35 +01:00
parent 720dc28c09
commit 40e2a747cf
36901 changed files with 5011519 additions and 0 deletions

View File

@ -0,0 +1,5 @@
from __future__ import annotations
from contourpy.util._build_config import build_config
__all__ = ["build_config"]

View File

@ -0,0 +1,60 @@
# _build_config.py.in is converted into _build_config.py during the meson build process.
from __future__ import annotations
def build_config() -> dict[str, str]:
"""
Return a dictionary containing build configuration settings.
All dictionary keys and values are strings, for example ``False`` is
returned as ``"False"``.
.. versionadded:: 1.1.0
"""
return dict(
# Python settings
python_version="3.12",
python_install_dir=r"c:/Lib/site-packages/",
python_path=r"C:/Users/runneradmin/AppData/Local/Temp/build-env-q46jz1_c/Scripts/python.exe",
# Package versions
contourpy_version="1.3.0",
meson_version="1.5.1",
mesonpy_version="0.16.0",
pybind11_version="2.13.5",
# Misc meson settings
meson_backend="ninja",
build_dir=r"D:/a/contourpy/contourpy/.mesonpy-q97jrs7l/lib/contourpy/util",
source_dir=r"D:/a/contourpy/contourpy/lib/contourpy/util",
cross_build="False",
# Build options
build_options=r"-Dbuildtype=release -Db_ndebug=if-release -Db_vscrt=mt '-Dcpp_link_args=['ucrt.lib','vcruntime.lib','/nodefaultlib:libucrt.lib','/nodefaultlib:libvcruntime.lib']' -Dvsenv=True '--native-file=D:/a/contourpy/contourpy/.mesonpy-q97jrs7l/meson-python-native-file.ini'",
buildtype="release",
cpp_std="c++17",
debug="False",
optimization="3",
vsenv="True",
b_ndebug="if-release",
b_vscrt="mt",
# C++ compiler
compiler_name="msvc",
compiler_version="19.40.33813",
linker_id="link",
compile_command="cl",
# Host machine
host_cpu="x86_64",
host_cpu_family="x86_64",
host_cpu_endian="little",
host_cpu_system="windows",
# Build machine, same as host machine if not a cross_build
build_cpu="x86_64",
build_cpu_family="x86_64",
build_cpu_endian="little",
build_cpu_system="windows",
)

View File

@ -0,0 +1,335 @@
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))

View File

@ -0,0 +1,74 @@
from __future__ import annotations
from typing import TYPE_CHECKING, cast
from contourpy import FillType, LineType
from contourpy.array import offsets_from_codes
from contourpy.convert import convert_lines
from contourpy.dechunk import dechunk_lines
if TYPE_CHECKING:
from contourpy._contourpy import (
CoordinateArray,
FillReturn,
LineReturn,
LineReturn_ChunkCombinedNan,
)
def filled_to_bokeh(
filled: FillReturn,
fill_type: FillType,
) -> tuple[list[list[CoordinateArray]], list[list[CoordinateArray]]]:
xs: list[list[CoordinateArray]] = []
ys: list[list[CoordinateArray]] = []
if fill_type in (FillType.OuterOffset, FillType.ChunkCombinedOffset,
FillType.OuterCode, FillType.ChunkCombinedCode):
have_codes = fill_type in (FillType.OuterCode, FillType.ChunkCombinedCode)
for points, offsets in zip(*filled):
if points is None:
continue
if have_codes:
offsets = offsets_from_codes(offsets)
xs.append([]) # New outer with zero or more holes.
ys.append([])
for i in range(len(offsets)-1):
xys = points[offsets[i]:offsets[i+1]]
xs[-1].append(xys[:, 0])
ys[-1].append(xys[:, 1])
elif fill_type in (FillType.ChunkCombinedCodeOffset, FillType.ChunkCombinedOffsetOffset):
for points, codes_or_offsets, outer_offsets in zip(*filled):
if points is None:
continue
for j in range(len(outer_offsets)-1):
if fill_type == FillType.ChunkCombinedCodeOffset:
codes = codes_or_offsets[outer_offsets[j]:outer_offsets[j+1]]
offsets = offsets_from_codes(codes) + outer_offsets[j]
else:
offsets = codes_or_offsets[outer_offsets[j]:outer_offsets[j+1]+1]
xs.append([]) # New outer with zero or more holes.
ys.append([])
for k in range(len(offsets)-1):
xys = points[offsets[k]:offsets[k+1]]
xs[-1].append(xys[:, 0])
ys[-1].append(xys[:, 1])
else:
raise RuntimeError(f"Conversion of FillType {fill_type} to Bokeh is not implemented")
return xs, ys
def lines_to_bokeh(
lines: LineReturn,
line_type: LineType,
) -> tuple[CoordinateArray | None, CoordinateArray | None]:
lines = convert_lines(lines, line_type, LineType.ChunkCombinedNan)
lines = dechunk_lines(lines, LineType.ChunkCombinedNan)
if TYPE_CHECKING:
lines = cast(LineReturn_ChunkCombinedNan, lines)
points = lines[0][0]
if points is None:
return None, None
else:
return points[:, 0], points[:, 1]

View File

@ -0,0 +1,78 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any
import numpy as np
if TYPE_CHECKING:
from contourpy._contourpy import CoordinateArray
def simple(
shape: tuple[int, int], want_mask: bool = False,
) -> tuple[CoordinateArray, CoordinateArray, CoordinateArray | np.ma.MaskedArray[Any, Any]]:
"""Return simple test data consisting of the sum of two gaussians.
Args:
shape (tuple(int, int)): 2D shape of data to return.
want_mask (bool, optional): Whether test data should be masked or not, default ``False``.
Return:
Tuple of 3 arrays: ``x``, ``y``, ``z`` test data, ``z`` will be masked if
``want_mask=True``.
"""
ny, nx = shape
x = np.arange(nx, dtype=np.float64)
y = np.arange(ny, dtype=np.float64)
x, y = np.meshgrid(x, y)
xscale = nx - 1.0
yscale = ny - 1.0
# z is sum of 2D gaussians.
amp = np.asarray([1.0, -1.0, 0.8, -0.9, 0.7])
mid = np.asarray([[0.4, 0.2], [0.3, 0.8], [0.9, 0.75], [0.7, 0.3], [0.05, 0.7]])
width = np.asarray([0.4, 0.2, 0.2, 0.2, 0.1])
z = np.zeros_like(x)
for i in range(len(amp)):
z += amp[i]*np.exp(-((x/xscale - mid[i, 0])**2 + (y/yscale - mid[i, 1])**2) / width[i]**2)
if want_mask:
mask = np.logical_or(
((x/xscale - 1.0)**2 / 0.2 + (y/yscale - 0.0)**2 / 0.1) < 1.0,
((x/xscale - 0.2)**2 / 0.02 + (y/yscale - 0.45)**2 / 0.08) < 1.0,
)
z = np.ma.array(z, mask=mask) # type: ignore[no-untyped-call]
return x, y, z
def random(
shape: tuple[int, int], seed: int = 2187, mask_fraction: float = 0.0,
) -> tuple[CoordinateArray, CoordinateArray, CoordinateArray | np.ma.MaskedArray[Any, Any]]:
"""Return random test data in the range 0 to 1.
Args:
shape (tuple(int, int)): 2D shape of data to return.
seed (int, optional): Seed for random number generator, default 2187.
mask_fraction (float, optional): Fraction of elements to mask, default 0.
Return:
Tuple of 3 arrays: ``x``, ``y``, ``z`` test data, ``z`` will be masked if
``mask_fraction`` is greater than zero.
"""
ny, nx = shape
x = np.arange(nx, dtype=np.float64)
y = np.arange(ny, dtype=np.float64)
x, y = np.meshgrid(x, y)
rng = np.random.default_rng(seed)
z = rng.uniform(size=shape)
if mask_fraction > 0.0:
mask_fraction = min(mask_fraction, 0.99)
mask = rng.uniform(size=shape) < mask_fraction
z = np.ma.array(z, mask=mask) # type: ignore[no-untyped-call]
return x, y, z

View File

@ -0,0 +1,534 @@
from __future__ import annotations
import io
from typing import TYPE_CHECKING, Any, cast
import matplotlib.collections as mcollections
import matplotlib.pyplot as plt
import numpy as np
from contourpy import FillType, LineType
from contourpy.convert import convert_filled, convert_lines
from contourpy.enum_util import as_fill_type, as_line_type
from contourpy.util.mpl_util import filled_to_mpl_paths, lines_to_mpl_paths
from contourpy.util.renderer import Renderer
if TYPE_CHECKING:
from collections.abc import Sequence
from matplotlib.axes import Axes
from matplotlib.figure import Figure
from numpy.typing import ArrayLike
import contourpy._contourpy as cpy
class MplRenderer(Renderer):
"""Utility renderer using Matplotlib 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, default ``(9, 9)``.
show_frame (bool, optional): Whether to show frame and axes ticks, default ``True``.
backend (str, optional): Matplotlib backend to use or ``None`` for default backend.
Default ``None``.
gridspec_kw (dict, optional): Gridspec keyword arguments to pass to ``plt.subplots``,
default None.
"""
_axes: Sequence[Axes]
_fig: Figure
_want_tight: bool
def __init__(
self,
nrows: int = 1,
ncols: int = 1,
figsize: tuple[float, float] = (9, 9),
show_frame: bool = True,
backend: str | None = None,
gridspec_kw: dict[str, Any] | None = None,
) -> None:
if backend is not None:
import matplotlib as mpl
mpl.use(backend)
kwargs: dict[str, Any] = {"figsize": figsize, "squeeze": False,
"sharex": True, "sharey": True}
if gridspec_kw is not None:
kwargs["gridspec_kw"] = gridspec_kw
else:
kwargs["subplot_kw"] = {"aspect": "equal"}
self._fig, axes = plt.subplots(nrows, ncols, **kwargs)
self._axes = axes.flatten()
if not show_frame:
for ax in self._axes:
ax.axis("off")
self._want_tight = True
def __del__(self) -> None:
if hasattr(self, "_fig"):
plt.close(self._fig)
def _autoscale(self) -> None:
# Using axes._need_autoscale attribute if need to autoscale before rendering after adding
# lines/filled. Only want to autoscale once per axes regardless of how many lines/filled
# added.
for ax in self._axes:
if getattr(ax, "_need_autoscale", False):
ax.autoscale_view(tight=True)
ax._need_autoscale = False # type: ignore[attr-defined]
if self._want_tight and len(self._axes) > 1:
self._fig.tight_layout()
def _get_ax(self, ax: Axes | int) -> Axes:
if isinstance(ax, int):
ax = self._axes[ax]
return ax
def filled(
self,
filled: cpy.FillReturn,
fill_type: FillType | str,
ax: Axes | int = 0,
color: str = "C0",
alpha: float = 0.7,
) -> None:
"""Plot filled contours on a single Axes.
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 string equivalent
ax (int or Maplotlib Axes, optional): Which axes to plot on, 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
``tab10`` colormap. Default ``"C0"``.
alpha (float, optional): Opacity to plot with, default ``0.7``.
"""
fill_type = as_fill_type(fill_type)
ax = self._get_ax(ax)
paths = filled_to_mpl_paths(filled, fill_type)
collection = mcollections.PathCollection(
paths, facecolors=color, edgecolors="none", lw=0, alpha=alpha)
ax.add_collection(collection)
ax._need_autoscale = True # type: ignore[attr-defined]
def grid(
self,
x: ArrayLike,
y: ArrayLike,
ax: Axes | 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 Axes.
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 Matplotlib Axes, optional): Which Axes to plot on, 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 ``tab10`` colormap.
Warning:
``quad_as_tri_alpha > 0`` plots all quads as though they are unmasked.
"""
ax = self._get_ax(ax)
x, y = self._grid_as_2d(x, y)
kwargs: dict[str, Any] = {"color": color, "alpha": alpha}
ax.plot(x, y, x.T, y.T, **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:])
ymid = 0.25*(y[:-1, :-1] + y[1:, :-1] + y[:-1, 1:] + y[1:, 1:])
kwargs["alpha"] = quad_as_tri_alpha
ax.plot(
np.stack((x[:-1, :-1], xmid, x[1:, 1:])).reshape((3, -1)),
np.stack((y[:-1, :-1], ymid, y[1:, 1:])).reshape((3, -1)),
np.stack((x[1:, :-1], xmid, x[:-1, 1:])).reshape((3, -1)),
np.stack((y[1:, :-1], ymid, y[:-1, 1:])).reshape((3, -1)),
**kwargs)
if point_color is not None:
ax.plot(x, y, color=point_color, alpha=alpha, marker="o", lw=0)
ax._need_autoscale = True # type: ignore[attr-defined]
def lines(
self,
lines: cpy.LineReturn,
line_type: LineType | str,
ax: Axes | int = 0,
color: str = "C0",
alpha: float = 1.0,
linewidth: float = 1,
) -> None:
"""Plot contour lines on a single Axes.
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 string equivalent.
ax (int or Matplotlib Axes, optional): Which Axes to plot on, 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
``tab10`` colormap. Default ``"C0"``.
alpha (float, optional): Opacity to plot lines with, default ``1.0``.
linewidth (float, optional): Width of lines, default ``1``.
"""
line_type = as_line_type(line_type)
ax = self._get_ax(ax)
paths = lines_to_mpl_paths(lines, line_type)
collection = mcollections.PathCollection(
paths, facecolors="none", edgecolors=color, lw=linewidth, alpha=alpha)
ax.add_collection(collection)
ax._need_autoscale = True # type: ignore[attr-defined]
def mask(
self,
x: ArrayLike,
y: ArrayLike,
z: ArrayLike | np.ma.MaskedArray[Any, Any],
ax: Axes | int = 0,
color: str = "black",
) -> None:
"""Plot masked out grid points as circles on a single Axes.
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 Matplotlib Axes, optional): Which Axes to plot on, 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
ax = self._get_ax(ax)
x, y = self._grid_as_2d(x, y)
ax.plot(x[mask], y[mask], "o", c=color)
def save(self, filename: str, transparent: bool = False) -> 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``.
"""
self._autoscale()
self._fig.savefig(filename, transparent=transparent)
def save_to_buffer(self) -> io.BytesIO:
"""Save plots to an ``io.BytesIO`` buffer.
Return:
BytesIO: PNG image buffer.
"""
self._autoscale()
buf = io.BytesIO()
self._fig.savefig(buf, format="png")
buf.seek(0)
return buf
def show(self) -> None:
"""Show plots in an interactive window, in the usual Matplotlib manner.
"""
self._autoscale()
plt.show()
def title(self, title: str, ax: Axes | int = 0, color: str | None = None) -> None:
"""Set the title of a single Axes.
Args:
title (str): Title text.
ax (int or Matplotlib Axes, optional): Which Axes 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
``tab10`` colormap. Default is ``None`` which uses Matplotlib's default title color
that depends on the stylesheet in use.
"""
if color:
self._get_ax(ax).set_title(title, color=color)
else:
self._get_ax(ax).set_title(title)
def z_values(
self,
x: ArrayLike,
y: ArrayLike,
z: ArrayLike,
ax: Axes | int = 0,
color: str = "green",
fmt: str = ".1f",
quad_as_tri: bool = False,
) -> None:
"""Show ``z`` values on a single Axes.
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 Matplotlib Axes, optional): Which Axes to plot on, 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
``tab10`` colormap. 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`` centers
of quads.
Warning:
``quad_as_tri=True`` shows z-values for all quads, even if masked.
"""
ax = self._get_ax(ax)
x, y = self._grid_as_2d(x, y)
z = np.asarray(z)
ny, nx = z.shape
for j in range(ny):
for i in range(nx):
ax.text(x[j, i], y[j, i], f"{z[j, i]:{fmt}}", ha="center", va="center",
color=color, clip_on=True)
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])
ax.text(xx, yy, f"{zz:{fmt}}", ha="center", va="center", color=color,
clip_on=True)
class MplTestRenderer(MplRenderer):
"""Test renderer implemented using Matplotlib.
No whitespace around plots and no spines/ticks displayed.
Uses Agg backend, so can only save to file/buffer, cannot call ``show()``.
"""
def __init__(
self,
nrows: int = 1,
ncols: int = 1,
figsize: tuple[float, float] = (9, 9),
) -> None:
gridspec = {
"left": 0.01,
"right": 0.99,
"top": 0.99,
"bottom": 0.01,
"wspace": 0.01,
"hspace": 0.01,
}
super().__init__(
nrows, ncols, figsize, show_frame=True, backend="Agg", gridspec_kw=gridspec,
)
for ax in self._axes:
ax.set_xmargin(0.0)
ax.set_ymargin(0.0)
ax.set_xticks([])
ax.set_yticks([])
self._want_tight = False
class MplDebugRenderer(MplRenderer):
"""Debug renderer implemented using Matplotlib.
Extends ``MplRenderer`` to add extra information to help in debugging such as markers, arrows,
text, etc.
"""
def __init__(
self,
nrows: int = 1,
ncols: int = 1,
figsize: tuple[float, float] = (9, 9),
show_frame: bool = True,
) -> None:
super().__init__(nrows, ncols, figsize, show_frame)
def _arrow(
self,
ax: Axes,
line_start: cpy.CoordinateArray,
line_end: cpy.CoordinateArray,
color: str,
alpha: float,
arrow_size: float,
) -> None:
mid = 0.5*(line_start + line_end)
along = line_end - line_start
along /= np.sqrt(np.dot(along, along)) # Unit vector.
right = np.asarray((along[1], -along[0]))
arrow = np.stack((
mid - (along*0.5 - right)*arrow_size,
mid + along*0.5*arrow_size,
mid - (along*0.5 + right)*arrow_size,
))
ax.plot(arrow[:, 0], arrow[:, 1], "-", c=color, alpha=alpha)
def filled(
self,
filled: cpy.FillReturn,
fill_type: FillType | str,
ax: Axes | int = 0,
color: str = "C1",
alpha: float = 0.7,
line_color: str = "C0",
line_alpha: float = 0.7,
point_color: str = "C0",
start_point_color: str = "red",
arrow_size: float = 0.1,
) -> None:
fill_type = as_fill_type(fill_type)
super().filled(filled, fill_type, ax, color, alpha)
if line_color is None and point_color is None:
return
ax = self._get_ax(ax)
filled = convert_filled(filled, fill_type, FillType.ChunkCombinedOffset)
# Lines.
if line_color is not None:
for points, offsets in zip(*filled):
if points is None:
continue
for start, end in zip(offsets[:-1], offsets[1:]):
xys = points[start:end]
ax.plot(xys[:, 0], xys[:, 1], c=line_color, alpha=line_alpha)
if arrow_size > 0.0:
n = len(xys)
for i in range(n-1):
self._arrow(ax, xys[i], xys[i+1], line_color, line_alpha, arrow_size)
# Points.
if point_color is not None:
for points, offsets in zip(*filled):
if points is None:
continue
mask = np.ones(offsets[-1], dtype=bool)
mask[offsets[1:]-1] = False # Exclude end points.
if start_point_color is not None:
start_indices = offsets[:-1]
mask[start_indices] = False # Exclude start points.
ax.plot(
points[:, 0][mask], points[:, 1][mask], "o", c=point_color, alpha=line_alpha)
if start_point_color is not None:
ax.plot(points[:, 0][start_indices], points[:, 1][start_indices], "o",
c=start_point_color, alpha=line_alpha)
def lines(
self,
lines: cpy.LineReturn,
line_type: LineType | str,
ax: Axes | int = 0,
color: str = "C0",
alpha: float = 1.0,
linewidth: float = 1,
point_color: str = "C0",
start_point_color: str = "red",
arrow_size: float = 0.1,
) -> None:
line_type = as_line_type(line_type)
super().lines(lines, line_type, ax, color, alpha, linewidth)
if arrow_size == 0.0 and point_color is None:
return
ax = self._get_ax(ax)
separate_lines = convert_lines(lines, line_type, LineType.Separate)
if TYPE_CHECKING:
separate_lines = cast(cpy.LineReturn_Separate, separate_lines)
if arrow_size > 0.0:
for line in separate_lines:
for i in range(len(line)-1):
self._arrow(ax, line[i], line[i+1], color, alpha, arrow_size)
if point_color is not None:
for line in separate_lines:
start_index = 0
end_index = len(line)
if start_point_color is not None:
ax.plot(line[0, 0], line[0, 1], "o", c=start_point_color, alpha=alpha)
start_index = 1
if line[0][0] == line[-1][0] and line[0][1] == line[-1][1]:
end_index -= 1
ax.plot(line[start_index:end_index, 0], line[start_index:end_index, 1], "o",
c=color, alpha=alpha)
def point_numbers(
self,
x: ArrayLike,
y: ArrayLike,
z: ArrayLike,
ax: Axes | int = 0,
color: str = "red",
) -> None:
ax = self._get_ax(ax)
x, y = self._grid_as_2d(x, y)
z = np.asarray(z)
ny, nx = z.shape
for j in range(ny):
for i in range(nx):
quad = i + j*nx
ax.text(x[j, i], y[j, i], str(quad), ha="right", va="top", color=color,
clip_on=True)
def quad_numbers(
self,
x: ArrayLike,
y: ArrayLike,
z: ArrayLike,
ax: Axes | int = 0,
color: str = "blue",
) -> None:
ax = self._get_ax(ax)
x, y = self._grid_as_2d(x, y)
z = np.asarray(z)
ny, nx = z.shape
for j in range(1, ny):
for i in range(1, nx):
quad = i + j*nx
xmid = x[j-1:j+1, i-1:i+1].mean()
ymid = y[j-1:j+1, i-1:i+1].mean()
ax.text(xmid, ymid, str(quad), ha="center", va="center", color=color, clip_on=True)
def z_levels(
self,
x: ArrayLike,
y: ArrayLike,
z: ArrayLike,
lower_level: float,
upper_level: float | None = None,
ax: Axes | int = 0,
color: str = "green",
) -> None:
ax = self._get_ax(ax)
x, y = self._grid_as_2d(x, y)
z = np.asarray(z)
ny, nx = z.shape
for j in range(ny):
for i in range(nx):
zz = z[j, i]
if upper_level is not None and zz > upper_level:
z_level = 2
elif zz > lower_level:
z_level = 1
else:
z_level = 0
ax.text(x[j, i], y[j, i], str(z_level), ha="left", va="bottom", color=color,
clip_on=True)

View File

@ -0,0 +1,76 @@
from __future__ import annotations
from typing import TYPE_CHECKING, cast
import matplotlib.path as mpath
import numpy as np
from contourpy import FillType, LineType
from contourpy.array import codes_from_offsets
if TYPE_CHECKING:
from contourpy._contourpy import FillReturn, LineReturn, LineReturn_Separate
def filled_to_mpl_paths(filled: FillReturn, fill_type: FillType) -> list[mpath.Path]:
if fill_type in (FillType.OuterCode, FillType.ChunkCombinedCode):
paths = [mpath.Path(points, codes) for points, codes in zip(*filled) if points is not None]
elif fill_type in (FillType.OuterOffset, FillType.ChunkCombinedOffset):
paths = [mpath.Path(points, codes_from_offsets(offsets))
for points, offsets in zip(*filled) if points is not None]
elif fill_type == FillType.ChunkCombinedCodeOffset:
paths = []
for points, codes, outer_offsets in zip(*filled):
if points is None:
continue
points = np.split(points, outer_offsets[1:-1])
codes = np.split(codes, outer_offsets[1:-1])
paths += [mpath.Path(p, c) for p, c in zip(points, codes)]
elif fill_type == FillType.ChunkCombinedOffsetOffset:
paths = []
for points, offsets, outer_offsets in zip(*filled):
if points is None:
continue
for i in range(len(outer_offsets)-1):
offs = offsets[outer_offsets[i]:outer_offsets[i+1]+1]
pts = points[offs[0]:offs[-1]]
paths += [mpath.Path(pts, codes_from_offsets(offs - offs[0]))]
else:
raise RuntimeError(f"Conversion of FillType {fill_type} to MPL Paths is not implemented")
return paths
def lines_to_mpl_paths(lines: LineReturn, line_type: LineType) -> list[mpath.Path]:
if line_type == LineType.Separate:
if TYPE_CHECKING:
lines = cast(LineReturn_Separate, lines)
paths = []
for line in lines:
# Drawing as Paths so that they can be closed correctly.
closed = line[0, 0] == line[-1, 0] and line[0, 1] == line[-1, 1]
paths.append(mpath.Path(line, closed=closed))
elif line_type in (LineType.SeparateCode, LineType.ChunkCombinedCode):
paths = [mpath.Path(points, codes) for points, codes in zip(*lines) if points is not None]
elif line_type == LineType.ChunkCombinedOffset:
paths = []
for points, offsets in zip(*lines):
if points is None:
continue
for i in range(len(offsets)-1):
line = points[offsets[i]:offsets[i+1]]
closed = line[0, 0] == line[-1, 0] and line[0, 1] == line[-1, 1]
paths.append(mpath.Path(line, closed=closed))
elif line_type == LineType.ChunkCombinedNan:
paths = []
for points in lines[0]:
if points is None:
continue
nan_offsets = np.nonzero(np.isnan(points[:, 0]))[0]
nan_offsets = np.concatenate([[-1], nan_offsets, [len(points)]])
for s, e in zip(nan_offsets[:-1], nan_offsets[1:]):
line = points[s+1:e]
closed = line[0, 0] == line[-1, 0] and line[0, 1] == line[-1, 1]
paths.append(mpath.Path(line, closed=closed))
else:
raise RuntimeError(f"Conversion of LineType {line_type} to MPL Paths is not implemented")
return paths

View File

@ -0,0 +1,166 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any
import numpy as np
if TYPE_CHECKING:
import io
from numpy.typing import ArrayLike
from contourpy._contourpy import CoordinateArray, FillReturn, FillType, LineReturn, LineType
class Renderer(ABC):
"""Abstract base class for renderers."""
def _grid_as_2d(self, x: ArrayLike, y: ArrayLike) -> tuple[CoordinateArray, CoordinateArray]:
x = np.asarray(x)
y = np.asarray(y)
if x.ndim == 1:
x, y = np.meshgrid(x, y)
return x, y
@abstractmethod
def filled(
self,
filled: FillReturn,
fill_type: FillType | str,
ax: Any = 0,
color: str = "C0",
alpha: float = 0.7,
) -> None:
pass
@abstractmethod
def grid(
self,
x: ArrayLike,
y: ArrayLike,
ax: Any = 0,
color: str = "black",
alpha: float = 0.1,
point_color: str | None = None,
quad_as_tri_alpha: float = 0,
) -> None:
pass
@abstractmethod
def lines(
self,
lines: LineReturn,
line_type: LineType | str,
ax: Any = 0,
color: str = "C0",
alpha: float = 1.0,
linewidth: float = 1,
) -> None:
pass
@abstractmethod
def mask(
self,
x: ArrayLike,
y: ArrayLike,
z: ArrayLike | np.ma.MaskedArray[Any, Any],
ax: Any = 0,
color: str = "black",
) -> None:
pass
def multi_filled(
self,
multi_filled: list[FillReturn],
fill_type: FillType | str,
ax: Any = 0,
color: str | None = None,
**kwargs: Any,
) -> None:
"""Plot multiple sets of filled contours on a single axes.
Args:
multi_filled (list of filled contour arrays): Multiple filled contour sets as returned
by :meth:`.ContourGenerator.multi_filled`.
fill_type (FillType or str): Type of filled data as returned by
:attr:`~.ContourGenerator.fill_type`, or string equivalent.
ax (int or Renderer-specific axes or figure object, optional): Which axes to plot on,
default ``0``.
color (str or None, optional): If a string color then this same color is used for all
filled contours. If ``None``, the default, then the filled contour sets use colors
from the ``tab10`` colormap in order, wrapping around to the beginning if more than
10 sets of filled contours are rendered.
kwargs: All other keyword argument are passed on to
:meth:`.Renderer.filled` unchanged.
.. versionadded:: 1.3.0
"""
if color is not None:
kwargs["color"] = color
for i, filled in enumerate(multi_filled):
if color is None:
kwargs["color"] = f"C{i % 10}"
self.filled(filled, fill_type, ax, **kwargs)
def multi_lines(
self,
multi_lines: list[LineReturn],
line_type: LineType | str,
ax: Any = 0,
color: str | None = None,
**kwargs: Any,
) -> None:
"""Plot multiple sets of contour lines on a single axes.
Args:
multi_lines (list of contour line arrays): Multiple contour line sets as returned by
:meth:`.ContourGenerator.multi_lines`.
line_type (LineType or str): Type of line data as returned by
:attr:`~.ContourGenerator.line_type`, or string equivalent.
ax (int or Renderer-specific axes or figure object, optional): Which axes to plot on,
default ``0``.
color (str or None, optional): If a string color then this same color is used for all
lines. If ``None``, the default, then the line sets use colors from the ``tab10``
colormap in order, wrapping around to the beginning if more than 10 sets of lines
are rendered.
kwargs: All other keyword argument are passed on to
:meth:`Renderer.lines` unchanged.
.. versionadded:: 1.3.0
"""
if color is not None:
kwargs["color"] = color
for i, lines in enumerate(multi_lines):
if color is None:
kwargs["color"] = f"C{i % 10}"
self.lines(lines, line_type, ax, **kwargs)
@abstractmethod
def save(self, filename: str, transparent: bool = False) -> None:
pass
@abstractmethod
def save_to_buffer(self) -> io.BytesIO:
pass
@abstractmethod
def show(self) -> None:
pass
@abstractmethod
def title(self, title: str, ax: Any = 0, color: str | None = None) -> None:
pass
@abstractmethod
def z_values(
self,
x: ArrayLike,
y: ArrayLike,
z: ArrayLike,
ax: Any = 0,
color: str = "green",
fmt: str = ".1f",
quad_as_tri: bool = False,
) -> None:
pass