new ip
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contourpy.util._build_config import build_config
|
||||
|
||||
__all__ = ["build_config"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,58 @@
|
||||
# _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"``.
|
||||
"""
|
||||
return dict(
|
||||
# Python settings
|
||||
python_version="3.8",
|
||||
python_install_dir=r"/usr/local/lib/python3.8/site-packages/",
|
||||
python_path=r"/tmp/build-env-dhtgjwl5/bin/python",
|
||||
|
||||
# Package versions
|
||||
contourpy_version="1.1.1",
|
||||
meson_version="1.2.1",
|
||||
mesonpy_version="0.14.0",
|
||||
pybind11_version="2.11.1",
|
||||
|
||||
# Misc meson settings
|
||||
meson_backend="ninja",
|
||||
build_dir=r"/project/.mesonpy-otn0iaj2/lib/contourpy/util",
|
||||
source_dir=r"/project/lib/contourpy/util",
|
||||
cross_build="False",
|
||||
|
||||
# Build options
|
||||
build_options=r"-Dbuildtype=release -Db_ndebug=if-release -Db_vscrt=md -Dvsenv=True --native-file=/project/.mesonpy-otn0iaj2/meson-python-native-file.ini",
|
||||
buildtype="release",
|
||||
cpp_std="c++17",
|
||||
debug="False",
|
||||
optimization="3",
|
||||
vsenv="True",
|
||||
b_ndebug="if-release",
|
||||
b_vscrt="from_buildtype",
|
||||
|
||||
# C++ compiler
|
||||
compiler_name="gcc",
|
||||
compiler_version="10.2.1",
|
||||
linker_id="ld.bfd",
|
||||
compile_command="c++",
|
||||
|
||||
# Host machine
|
||||
host_cpu="x86_64",
|
||||
host_cpu_family="x86_64",
|
||||
host_cpu_endian="little",
|
||||
host_cpu_system="linux",
|
||||
|
||||
# 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="linux",
|
||||
)
|
||||
@@ -0,0 +1,329 @@
|
||||
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 import FillType, LineType
|
||||
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._contourpy import FillReturn, LineReturn
|
||||
|
||||
|
||||
class BokehRenderer(Renderer):
|
||||
_figures: list[figure]
|
||||
_layout: GridPlot
|
||||
_palette: Palette
|
||||
_want_svg: bool
|
||||
|
||||
"""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:`~contourpy.util.bokeh_renderer.BokehRenderer`, unlike
|
||||
:class:`~contourpy.util.mpl_renderer.MplRenderer`, needs to be told in advance if output to
|
||||
SVG format will be required later, otherwise it will assume PNG output.
|
||||
"""
|
||||
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,
|
||||
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
|
||||
:func:`~contourpy.ContourGenerator.filled`.
|
||||
fill_type (FillType): Type of ``filled`` data, as returned by
|
||||
:attr:`~contourpy.ContourGenerator.fill_type`.
|
||||
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``.
|
||||
"""
|
||||
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 = [row for row in x] + [row for row in x.T]
|
||||
ys = [row for row in y] + [row for row in y.T]
|
||||
kwargs = dict(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(
|
||||
[row for row in np.stack((x[:-1, :-1].ravel(), xmid, x[1:, 1:].ravel()), axis=1)],
|
||||
[row for row in np.stack((y[:-1, :-1].ravel(), ymid, y[1:, 1:].ravel()), axis=1)],
|
||||
**kwargs)
|
||||
fig.multi_line(
|
||||
[row for row in np.stack((x[:-1, 1:].ravel(), xmid, x[1:, :-1].ravel()), axis=1)],
|
||||
[row for row in 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,
|
||||
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
|
||||
:func:`~contourpy.ContourGenerator.lines`.
|
||||
line_type (LineType): Type of ``lines`` data, as returned by
|
||||
:attr:`~contourpy.ContourGenerator.line_type`.
|
||||
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.
|
||||
"""
|
||||
fig = self._get_figure(ax)
|
||||
color = self._convert_color(color)
|
||||
xs, ys = lines_to_bokeh(lines, line_type)
|
||||
if len(xs) > 0:
|
||||
fig.multi_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.
|
||||
|
||||
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.
|
||||
|
||||
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 = dict(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))
|
||||
@@ -0,0 +1,90 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from contourpy import FillType, LineType
|
||||
from contourpy.util.mpl_util import mpl_codes_to_offsets
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from contourpy._contourpy import (
|
||||
CoordinateArray, FillReturn, LineReturn, LineReturn_Separate, LineReturn_SeparateCode,
|
||||
)
|
||||
|
||||
|
||||
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 = mpl_codes_to_offsets(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 = mpl_codes_to_offsets(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[list[CoordinateArray], list[CoordinateArray]]:
|
||||
xs: list[CoordinateArray] = []
|
||||
ys: list[CoordinateArray] = []
|
||||
|
||||
if line_type == LineType.Separate:
|
||||
if TYPE_CHECKING:
|
||||
lines = cast(LineReturn_Separate, lines)
|
||||
for line in lines:
|
||||
xs.append(line[:, 0])
|
||||
ys.append(line[:, 1])
|
||||
elif line_type == LineType.SeparateCode:
|
||||
if TYPE_CHECKING:
|
||||
lines = cast(LineReturn_SeparateCode, lines)
|
||||
for line in lines[0]:
|
||||
xs.append(line[:, 0])
|
||||
ys.append(line[:, 1])
|
||||
elif line_type in (LineType.ChunkCombinedCode, LineType.ChunkCombinedOffset):
|
||||
for points, offsets in zip(*lines):
|
||||
if points is None:
|
||||
continue
|
||||
if line_type == LineType.ChunkCombinedCode:
|
||||
offsets = mpl_codes_to_offsets(offsets)
|
||||
|
||||
for i in range(len(offsets)-1):
|
||||
line = points[offsets[i]:offsets[i+1]]
|
||||
xs.append(line[:, 0])
|
||||
ys.append(line[:, 1])
|
||||
else:
|
||||
raise RuntimeError(f"Conversion of LineType {line_type} to Bokeh is not implemented")
|
||||
|
||||
return xs, ys
|
||||
@@ -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..
|
||||
|
||||
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
|
||||
@@ -0,0 +1,613 @@
|
||||
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.util.mpl_util import filled_to_mpl_paths, lines_to_mpl_paths, mpl_codes_to_offsets
|
||||
from contourpy.util.renderer import Renderer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from matplotlib.axes import Axes
|
||||
from matplotlib.figure import Figure
|
||||
from numpy.typing import ArrayLike
|
||||
|
||||
import contourpy._contourpy as cpy
|
||||
|
||||
|
||||
class MplRenderer(Renderer):
|
||||
_axes: Axes
|
||||
_fig: Figure
|
||||
_want_tight: bool
|
||||
|
||||
"""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.
|
||||
"""
|
||||
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
|
||||
matplotlib.use(backend)
|
||||
|
||||
kwargs = dict(figsize=figsize, squeeze=False, sharex=True, sharey=True)
|
||||
if gridspec_kw is not None:
|
||||
kwargs["gridspec_kw"] = gridspec_kw
|
||||
else:
|
||||
kwargs["subplot_kw"] = dict(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
|
||||
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,
|
||||
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
|
||||
:func:`~contourpy.ContourGenerator.filled`.
|
||||
fill_type (FillType): Type of ``filled`` data, as returned by
|
||||
:attr:`~contourpy.ContourGenerator.fill_type`.
|
||||
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``.
|
||||
"""
|
||||
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
|
||||
|
||||
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(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
|
||||
|
||||
def lines(
|
||||
self,
|
||||
lines: cpy.LineReturn,
|
||||
line_type: LineType,
|
||||
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
|
||||
:func:`~contourpy.ContourGenerator.lines`.
|
||||
line_type (LineType): Type of ``lines`` data, as returned by
|
||||
:attr:`~contourpy.ContourGenerator.line_type`.
|
||||
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``.
|
||||
"""
|
||||
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
|
||||
|
||||
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_to_lists_of_points_and_offsets(
|
||||
self,
|
||||
filled: cpy.FillReturn,
|
||||
fill_type: FillType,
|
||||
) -> tuple[list[cpy.PointArray], list[cpy.OffsetArray]]:
|
||||
if fill_type == FillType.OuterCode:
|
||||
if TYPE_CHECKING:
|
||||
filled = cast(cpy.FillReturn_OuterCode, filled)
|
||||
all_points = filled[0]
|
||||
all_offsets = [mpl_codes_to_offsets(codes) for codes in filled[1]]
|
||||
elif fill_type == FillType.ChunkCombinedCode:
|
||||
if TYPE_CHECKING:
|
||||
filled = cast(cpy.FillReturn_ChunkCombinedCode, filled)
|
||||
all_points = [points for points in filled[0] if points is not None]
|
||||
all_offsets = [mpl_codes_to_offsets(codes) for codes in filled[1] if codes is not None]
|
||||
elif fill_type == FillType.OuterOffset:
|
||||
if TYPE_CHECKING:
|
||||
filled = cast(cpy.FillReturn_OuterOffset, filled)
|
||||
all_points = filled[0]
|
||||
all_offsets = filled[1]
|
||||
elif fill_type == FillType.ChunkCombinedOffset:
|
||||
if TYPE_CHECKING:
|
||||
filled = cast(cpy.FillReturn_ChunkCombinedOffset, filled)
|
||||
all_points = [points for points in filled[0] if points is not None]
|
||||
all_offsets = [offsets for offsets in filled[1] if offsets is not None]
|
||||
elif fill_type == FillType.ChunkCombinedCodeOffset:
|
||||
if TYPE_CHECKING:
|
||||
filled = cast(cpy.FillReturn_ChunkCombinedCodeOffset, filled)
|
||||
all_points = []
|
||||
all_offsets = []
|
||||
for points, codes, outer_offsets in zip(*filled):
|
||||
if points is None:
|
||||
continue
|
||||
if TYPE_CHECKING:
|
||||
assert codes is not None and outer_offsets is not None
|
||||
all_points += np.split(points, outer_offsets[1:-1])
|
||||
all_codes = np.split(codes, outer_offsets[1:-1])
|
||||
all_offsets += [mpl_codes_to_offsets(codes) for codes in all_codes]
|
||||
elif fill_type == FillType.ChunkCombinedOffsetOffset:
|
||||
if TYPE_CHECKING:
|
||||
filled = cast(cpy.FillReturn_ChunkCombinedOffsetOffset, filled)
|
||||
all_points = []
|
||||
all_offsets = []
|
||||
for points, offsets, outer_offsets in zip(*filled):
|
||||
if points is None:
|
||||
continue
|
||||
if TYPE_CHECKING:
|
||||
assert offsets is not None and outer_offsets is not None
|
||||
for i in range(len(outer_offsets)-1):
|
||||
offs = offsets[outer_offsets[i]:outer_offsets[i+1]+1]
|
||||
all_points.append(points[offs[0]:offs[-1]])
|
||||
all_offsets.append(offs - offs[0])
|
||||
else:
|
||||
raise RuntimeError(f"Rendering FillType {fill_type} not implemented")
|
||||
|
||||
return all_points, all_offsets
|
||||
|
||||
def _lines_to_list_of_points(
|
||||
self, lines: cpy.LineReturn, line_type: LineType,
|
||||
) -> list[cpy.PointArray]:
|
||||
if line_type == LineType.Separate:
|
||||
if TYPE_CHECKING:
|
||||
lines = cast(cpy.LineReturn_Separate, lines)
|
||||
all_lines = lines
|
||||
elif line_type == LineType.SeparateCode:
|
||||
if TYPE_CHECKING:
|
||||
lines = cast(cpy.LineReturn_SeparateCode, lines)
|
||||
all_lines = lines[0]
|
||||
elif line_type == LineType.ChunkCombinedCode:
|
||||
if TYPE_CHECKING:
|
||||
lines = cast(cpy.LineReturn_ChunkCombinedCode, lines)
|
||||
all_lines = []
|
||||
for points, codes in zip(*lines):
|
||||
if points is not None:
|
||||
if TYPE_CHECKING:
|
||||
assert codes is not None
|
||||
offsets = mpl_codes_to_offsets(codes)
|
||||
for i in range(len(offsets)-1):
|
||||
all_lines.append(points[offsets[i]:offsets[i+1]])
|
||||
elif line_type == LineType.ChunkCombinedOffset:
|
||||
if TYPE_CHECKING:
|
||||
lines = cast(cpy.LineReturn_ChunkCombinedOffset, lines)
|
||||
all_lines = []
|
||||
for points, all_offsets in zip(*lines):
|
||||
if points is not None:
|
||||
if TYPE_CHECKING:
|
||||
assert all_offsets is not None
|
||||
for i in range(len(all_offsets)-1):
|
||||
all_lines.append(points[all_offsets[i]:all_offsets[i+1]])
|
||||
else:
|
||||
raise RuntimeError(f"Rendering LineType {line_type} not implemented")
|
||||
|
||||
return all_lines
|
||||
|
||||
def filled(
|
||||
self,
|
||||
filled: cpy.FillReturn,
|
||||
fill_type: FillType,
|
||||
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:
|
||||
super().filled(filled, fill_type, ax, color, alpha)
|
||||
|
||||
if line_color is None and point_color is None:
|
||||
return
|
||||
|
||||
ax = self._get_ax(ax)
|
||||
all_points, all_offsets = self._filled_to_lists_of_points_and_offsets(filled, fill_type)
|
||||
|
||||
# Lines.
|
||||
if line_color is not None:
|
||||
for points, offsets in zip(all_points, all_offsets):
|
||||
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(all_points, all_offsets):
|
||||
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,
|
||||
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:
|
||||
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)
|
||||
all_lines = self._lines_to_list_of_points(lines, line_type)
|
||||
|
||||
if arrow_size > 0.0:
|
||||
for line in all_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 all_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], z_level, ha="left", va="bottom", color=color,
|
||||
clip_on=True)
|
||||
@@ -0,0 +1,79 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
import matplotlib.path as mpath
|
||||
import numpy as np
|
||||
|
||||
from contourpy import FillType, LineType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from contourpy._contourpy import (
|
||||
CodeArray, FillReturn, LineReturn, LineReturn_Separate, OffsetArray,
|
||||
)
|
||||
|
||||
|
||||
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, offsets_to_mpl_codes(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, offsets_to_mpl_codes(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))
|
||||
else:
|
||||
raise RuntimeError(f"Conversion of LineType {line_type} to MPL Paths is not implemented")
|
||||
return paths
|
||||
|
||||
|
||||
def mpl_codes_to_offsets(codes: CodeArray) -> OffsetArray:
|
||||
offsets = np.nonzero(codes == 1)[0].astype(np.uint32)
|
||||
offsets = np.append(offsets, len(codes))
|
||||
return offsets
|
||||
|
||||
|
||||
def offsets_to_mpl_codes(offsets: OffsetArray) -> CodeArray:
|
||||
codes = np.full(offsets[-1]-offsets[0], 2, dtype=np.uint8) # LINETO = 2
|
||||
codes[offsets[:-1]] = 1 # MOVETO = 1
|
||||
codes[offsets[1:]-1] = 79 # CLOSEPOLY 79
|
||||
return codes
|
||||
@@ -0,0 +1,106 @@
|
||||
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, defining the interface that they must implement."""
|
||||
|
||||
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
|
||||
|
||||
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,
|
||||
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,
|
||||
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
|
||||
|
||||
@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
|
||||
Reference in New Issue
Block a user