Source code for whitecanvas.layers.primitive.inf_curve

from __future__ import annotations

from typing import Callable, TYPE_CHECKING, Generic
from typing_extensions import Concatenate, ParamSpec
import inspect

import math
import numpy as np

from whitecanvas.backend import Backend
from whitecanvas.types import LineStyle, ColorType, Rect
from whitecanvas.layers.primitive.line import MonoLine

if TYPE_CHECKING:
    from typing import NoReturn
    from whitecanvas.canvas import Canvas

_P = ParamSpec("_P")


[docs]class InfCurve(MonoLine, Generic[_P]): def __init__( self, model: Callable[[Concatenate[np.ndarray, _P]], np.ndarray], *, bounds: tuple[float, float] = (-np.inf, np.inf), name: str | None = None, color: ColorType = "blue", alpha: float = 1, width: float = 1, style: LineStyle | str = LineStyle.SOLID, antialias: bool = True, backend: Backend | str | None = None, ): _lower, _upper = bounds if not np.isfinite(_lower): _lower = 0 if not np.isfinite(_upper): _upper = 1 super().__init__(name=name) xdata = np.array([_lower, _upper]) self._sig = inspect.signature(model) try: self._sig.bind(xdata) except TypeError: ydata = np.zeros_like(xdata) self._y_hint = None self._params_ready = False else: ydata = model(xdata) self._y_hint = ydata.min(), ydata.max() self._params_ready = True self._backend = self._create_backend(Backend(backend), xdata, ydata) self.update( color=color, width=width, style=style, alpha=alpha, antialias=antialias ) # fmt: skip self._bounds = bounds self._model = model self._args = () self._kwargs = {} self._linspace_num = 256 @property def data(self) -> NoReturn: raise NotImplementedError("Cannot get data from an InfCurve layer.")
[docs] def set_data(self, xdata=None, ydata=None) -> NoReturn: raise NotImplementedError("Cannot set data to an InfCurve layer.")
[docs] def with_params(self, *args: _P.args, **kwargs: _P.kwargs) -> None: """Set the parameters of the model function.""" xdata, _ = self._backend._plt_get_data() ydata = self._model(xdata, *args, **kwargs) self._backend._plt_set_data(xdata, ydata) self._args, self._kwargs = args, kwargs self._params_ready = True
@property def model(self) -> Callable[[np.ndarray], np.ndarray]: """The model function of the layer.""" return self._model def _connect_canvas(self, canvas: Canvas): canvas.x.events.lim.connect(self._recalculate_line) self._recalculate_line(canvas.x.lim) super()._connect_canvas(canvas) def _disconnect_canvas(self, canvas: Canvas): canvas.x.events.lim.disconnect(self._recalculate_line) super()._disconnect_canvas(canvas) def _recalculate_line(self, lim: tuple[float, float]) -> None: x0, x1 = lim b0, b1 = self._bounds x0 = max(x0, b0) x1 = min(x1, b1) if x0 >= x1: xdata, ydata = np.array([]), np.array([]) else: xdata = np.linspace(x0, x1, self._linspace_num) if self._params_ready: ydata = self._model(xdata, *self._args, **self._kwargs) else: ydata = self._backend._plt_get_data()[1] self._backend._plt_set_data(xdata, ydata)
[docs]class InfLine(MonoLine): def __init__( self, pos: tuple[float, float] = (0, 0), angle: float = 0.0, *, name: str | None = None, color: ColorType = "blue", alpha: float = 1, width: float = 1, style: LineStyle | str = LineStyle.SOLID, antialias: bool = True, backend: Backend | str | None = None, ): self._is_vline = angle % 180 == 90 if self._is_vline: self._tan = 0 # not used self._intercept = pos[0] else: _radian = math.radians(angle) % (2 * math.pi) self._tan = math.tan(_radian) self._intercept = pos[1] - self._tan * pos[0] self._pos = pos super().__init__(name=name) self._backend = self._create_backend(Backend(backend), np.zeros(1), np.zeros(1)) self.update( color=color, width=width, style=style, alpha=alpha, antialias=antialias, ) # fmt: skip self._last_rect = Rect(0, 0, 0, 0) @property def data(self) -> NoReturn: raise NotImplementedError("Cannot get data from an Line layer.")
[docs] def set_data(self, xdata=None, ydata=None) -> NoReturn: raise NotImplementedError("Cannot set data to an Line layer.")
@property def pos(self) -> tuple[float, float]: """One of the points on the line.""" return self._pos @pos.setter def pos(self, pos: tuple[float, float]): self._pos = pos self._recalculate_line(self._last_rect) def _connect_canvas(self, canvas: Canvas): canvas.events.lims.connect(self._recalculate_line) self._recalculate_line(canvas.lims) self._last_rect = canvas.lims super()._connect_canvas(canvas) def _disconnect_canvas(self, canvas: Canvas): canvas.events.lims.disconnect(self._recalculate_line) super()._disconnect_canvas(canvas) def _recalculate_line(self, rect: Rect) -> None: x0, x1, y0, y1 = rect self._last_rect = rect if self._is_vline: x = self._intercept xdata = np.array([x, x]) ydata = np.array([y0, y1]) else: xdata = np.array([x0, x1]) ydata = self._tan * xdata + self._intercept self._backend._plt_set_data(xdata, ydata)