Source code for plotpy.items.shape.axis

# -*- coding: utf-8 -*-

from __future__ import annotations

import math
from typing import TYPE_CHECKING

import numpy as np
from guidata.dataset import update_dataset
from guidata.utils.misc import assert_interfaces_valid
from qtpy import QtCore as QC
from qtpy import QtGui as QG

from plotpy.config import CONF, _
from plotpy.items.shape.polygon import PolygonShape
from plotpy.styles.shape import AxesShapeParam

if TYPE_CHECKING:
    import guidata.io
    import qwt.scale_map
    from qtpy.QtCore import QRectF
    from qtpy.QtGui import QPainter

    from plotpy.styles.base import ItemParameters
    from plotpy.styles.shape import ShapeParam


[docs] class Axes(PolygonShape): """Axes shape Args: p0: First point (0,1) p1: Second point (1,1) p2: Third point (0,0) axesparam: Axes parameters shapeparam: Shape parameters """ CLOSED: bool = True _icon_name = "gtaxes.png" def __init__( self, p0: tuple[float, float] = (0, 0), p1: tuple[float, float] = (0, 0), p2: tuple[float, float] = (0, 0), axesparam: AxesShapeParam = None, shapeparam: ShapeParam = None, ) -> None: super().__init__(shapeparam=shapeparam) self.set_rect(p0, p1, p2) self.arrow_angle = 15.0 # degrees self.arrow_size = 0.05 # % of axe length self.x_pen = self.pen self.x_brush = self.brush self.y_pen = self.pen self.y_brush = self.brush if axesparam is None: self.axesparam = AxesShapeParam(_("Axes"), icon="gtaxes.png") else: self.axesparam = axesparam self.axesparam.update_param(self) def __reduce__(self) -> tuple: """Reduce object to picklable state""" self.axesparam.update_param(self) state = (self.shapeparam, self.axesparam, self.points, self.z()) return (self.__class__, (), state) def __setstate__(self, state: tuple) -> None: """Set object state from pickled state""" shapeparam, axesparam, points, z = state self.points = points self.setZ(z) self.shapeparam: ShapeParam = shapeparam self.shapeparam.update_item(self) self.axesparam: AxesShapeParam = axesparam self.axesparam.update_item(self)
[docs] def serialize( self, writer: guidata.io.HDF5Writer | guidata.io.INIWriter | guidata.io.JSONWriter, ) -> None: """Serialize object to HDF5 writer Args: writer: HDF5, INI or JSON writer """ super().serialize(writer) self.axesparam.update_param(self) writer.write(self.axesparam, group_name="axesparam")
[docs] def deserialize( self, reader: guidata.io.HDF5Reader | guidata.io.INIReader | guidata.io.JSONReader, ) -> None: """Deserialize object from HDF5 reader Args: reader: HDF5, INI or JSON reader """ super().deserialize(reader) self.axesparam = AxesShapeParam(_("Axes"), icon="gtaxes.png") reader.read("axesparam", instance=self.axesparam) self.axesparam.update_item(self)
[docs] def get_transform_matrix(self, dx: float = 1.0, dy: float = 1.0) -> np.ndarray: """Return the transformation matrix Args: dx: X scale dy: Y scale Returns: Transformation matrix """ p0, p1, _p3, p2 = [np.array([p[0], p[1], 1.0]) for p in self.points] matrix = np.array([(p1 - p0) / dx, (p2 - p0) / dy, p0]) if abs(np.linalg.det(matrix)) > 1e-12: return np.linalg.inv(matrix)
[docs] def set_rect( self, p0: tuple[float, float], p1: tuple[float, float], p2: tuple[float, float] ) -> None: """Set the coordinates of the axes Args: p0: First point (0,1) p1: Second point (1,1) p2: Third point (0,0) """ p3x = p1[0] + p2[0] - p0[0] p3y = p1[1] + p2[1] - p0[1] self.set_points([p0, p1, (p3x, p3y), p2])
[docs] def set_style(self, section: str, option: str) -> None: """Set style for this item Args: section: Section option: Option """ PolygonShape.set_style(self, section, option + "/border") self.axesparam.read_config(CONF, section, option) self.axesparam.update_item(self)
[docs] def move_point_to( self, handle: int, pos: tuple[float, float], ctrl: bool = False ) -> None: """Move a handle as returned by hit_test to the new position Args: handle: Handle pos: Position ctrl: True if <Ctrl> button is being pressed, False otherwise """ _nx, _ny = pos p0, p1, _p3, p2 = list(self.points) d1x = p1[0] - p0[0] d1y = p1[1] - p0[1] d2x = p2[0] - p0[0] d2y = p2[1] - p0[1] if handle == 0: pp0 = pos pp1 = pos[0] + d1x, pos[1] + d1y pp2 = pos[0] + d2x, pos[1] + d2y elif handle == 1: pp0 = p0 pp1 = pos pp2 = p2 elif handle == 3: pp0 = p0 pp1 = p1 pp2 = pos elif handle == 2: # find (a,b) such that p3 = a*d1 + b*d2 + p0 d3x = pos[0] - p0[0] d3y = pos[1] - p0[1] det = d1x * d2y - d2x * d1y if abs(det) < 1e-6: # reset d1x = d2y = 1.0 d1y = d2x = 0.0 det = 1.0 a = (d2y * d3x - d2x * d3y) / det b = (-d1y * d3x + d1x * d3y) / det _pp3 = pos pp1 = p0[0] + a * d1x, p0[1] + a * d1y pp2 = p0[0] + b * d2x, p0[1] + b * d2y pp0 = p0 self.set_rect(pp0, pp1, pp2) if self.plot(): self.plot().SIG_AXES_CHANGED.emit(self)
[docs] def move_shape(self, old_pos: QC.QPointF, new_pos: QC.QPointF) -> None: """Translate the shape such that old_pos becomes new_pos in axis coordinates Args: old_pos: Old position new_pos: New position """ PolygonShape.move_shape(self, old_pos, new_pos) if self.plot(): self.plot().SIG_AXES_CHANGED.emit(self)
[docs] def draw( self, painter: QPainter, xMap: qwt.scale_map.QwtScaleMap, yMap: qwt.scale_map.QwtScaleMap, canvasRect: QRectF, ) -> None: """Draw the item Args: painter: Painter xMap: X axis scale map yMap: Y axis scale map canvasRect: Canvas rectangle """ PolygonShape.draw(self, painter, xMap, yMap, canvasRect) p0, p1, _, p2 = list(self.points) points = self.transform_points(xMap, yMap) # points is a QPolygonF painter.setPen(self.x_pen) painter.setBrush(self.x_brush) painter.drawLine(points.at(0), points.at(1)) self.draw_arrow(painter, xMap, yMap, p0, p1) painter.setPen(self.y_pen) painter.setBrush(self.y_brush) painter.drawLine(points.at(0), points.at(3)) self.draw_arrow(painter, xMap, yMap, p0, p2)
[docs] def draw_arrow( self, painter: QPainter, xMap: qwt.scale_map.QwtScaleMap, yMap: qwt.scale_map.QwtScaleMap, p0: tuple[float, float], p1: tuple[float, float], ) -> None: """Draw an arrow Args: painter: Painter xMap: X axis scale map yMap: Y axis scale map p0: First point p1: Second point """ sz = self.arrow_size angle = math.pi * self.arrow_angle / 180.0 ca, sa = math.cos(angle), math.sin(angle) d1x = xMap.transform(p1[0]) - xMap.transform(p0[0]) d1y = yMap.transform(p1[1]) - yMap.transform(p0[1]) norm = math.sqrt(d1x**2 + d1y**2) if abs(norm) < 1e-6: return d1x *= sz / norm d1y *= sz / norm n1x = -d1y n1y = d1x # arrow : a0 - a1 == p1 - a2 a1x = xMap.transform(p1[0]) a1y = yMap.transform(p1[1]) a0x = a1x - ca * d1x + sa * n1x a0y = a1y - ca * d1y + sa * n1y a2x = a1x - ca * d1x - sa * n1x a2y = a1y - ca * d1y - sa * n1y poly = QG.QPolygonF() poly.append(QC.QPointF(a0x, a0y)) poly.append(QC.QPointF(a1x, a1y)) poly.append(QC.QPointF(a2x, a2y)) painter.drawPolygon(poly)
[docs] def update_item_parameters(self) -> None: """Update item parameters (dataset) from object properties""" self.axesparam.update_param(self)
[docs] def get_item_parameters(self, itemparams: ItemParameters) -> None: """ Appends datasets to the list of DataSets describing the parameters used to customize apearance of this item Args: itemparams: Item parameters """ PolygonShape.get_item_parameters(self, itemparams) self.update_item_parameters() itemparams.add("AxesShapeParam", self, self.axesparam)
[docs] def set_item_parameters(self, itemparams: ItemParameters) -> None: """ Change the appearance of this item according to the parameter set provided Args: itemparams: Item parameters """ PolygonShape.set_item_parameters(self, itemparams) update_dataset( self.axesparam, itemparams.get("AxesShapeParam"), visible_only=True ) self.axesparam.update_item(self)
assert_interfaces_valid(Axes)