# -*- coding: utf-8 -*-
#
# Copyright © 2009-2010 CEA
# Pierre Raybaut
# Licensed under the terms of the CECILL License
# (see plotpy/__init__.py for details)
# pylint: disable=C0103
"""
plotpy.annotations
------------------
The `annotations` module provides annotated shapes:
* :py:class:`plotpy.annotations.AnnotatedPoint`
* :py:class:`plotpy.annotations.AnnotatedSegment`
* :py:class:`plotpy.annotations.AnnotatedRectangle`
* :py:class:`plotpy.annotations.AnnotatedObliqueRectangle`
* :py:class:`plotpy.annotations.AnnotatedEllipse`
* :py:class:`plotpy.annotations.AnnotatedCircle`
An annotated shape is a plot item (derived from QwtPlotItem) that may be
displayed on a 2D plotting widget like :py:class:`plotpy.curve.CurvePlot`
or :py:class:`plotpy.image.ImagePlot`.
.. seealso:: module :py:mod:`plotpy.shapes`
Examples
~~~~~~~~
An annotated shape may be created:
* from the associated plot item class (e.g. `AnnotatedCircle` to
create an annotated circle): the item properties are then assigned
by creating the appropriate style parameters object
(:py:class:`plotpy.styles.AnnotationParam`)
>>> from plotpy.annotations import AnnotatedCircle
>>> from plotpy.styles import AnnotationParam
>>> param = AnnotationParam()
>>> param.title = 'My circle'
>>> circle_item = AnnotatedCircle(0., 2., 4., 0., param)
* or using the `plot item builder` (see :py:func:`plotpy.builder.make`):
>>> from plotpy.builder import make
>>> circle_item = make.annotated_circle(0., 2., 4., 0., title='My circle')
Reference
~~~~~~~~~
.. autoclass:: AnnotatedPoint
:members:
:inherited-members:
.. autoclass:: AnnotatedSegment
:members:
:inherited-members:
.. autoclass:: AnnotatedRectangle
:members:
:inherited-members:
.. autoclass:: AnnotatedObliqueRectangle
:members:
:inherited-members:
.. autoclass:: AnnotatedEllipse
:members:
:inherited-members:
.. autoclass:: AnnotatedCircle
:members:
:inherited-members:
"""
from __future__ import unicode_literals
import numpy as np
from guidata.utils import update_dataset, assert_interfaces_valid
# Local imports
from plotpy.config import CONF, _
from plotpy.styles import LabelParam, AnnotationParam
from plotpy.shapes import (AbstractShape, RectangleShape, EllipseShape,
SegmentShape, PointShape, ObliqueRectangleShape)
from plotpy.label import DataInfoLabel
from plotpy.interfaces import IBasePlotItem, IShapeItemType, ISerializableType
from plotpy.geometry import (compute_center, compute_rect_size,
compute_distance, compute_angle)
from plotpy.baseplot import canvas_to_axes
class AnnotatedShape(AbstractShape):
"""
Construct an annotated shape with properties set with
*annotationparam* (see :py:class:`plotpy.styles.AnnotationParam`)
"""
__implements__ = (IBasePlotItem, ISerializableType)
SHAPE_CLASS = None
LABEL_ANCHOR = None
def __init__(self, annotationparam=None):
AbstractShape.__init__(self)
assert self.LABEL_ANCHOR is not None
self.shape = self.create_shape()
self.label = self.create_label()
self.area_computations_visible = True
if annotationparam is None:
self.annotationparam = AnnotationParam(_("Annotation"),
icon="annotation.png")
else:
self.annotationparam = annotationparam
self.annotationparam.update_annotation(self)
def types(self):
return (IShapeItemType, ISerializableType)
def __reduce__(self):
self.annotationparam.update_param(self)
state = (self.shape, self.label, self.annotationparam)
return (self.__class__, (), state)
def __setstate__(self, state):
shape, label, param = state
self.shape = shape
self.label = label
self.annotationparam = param
self.annotationparam.update_annotation(self)
def serialize(self, writer):
"""Serialize object to HDF5 writer"""
writer.write(self.annotationparam, group_name='annotationparam')
self.shape.serialize(writer)
self.label.serialize(writer)
def deserialize(self, reader):
"""Deserialize object from HDF5 reader"""
self.annotationparam = AnnotationParam(_("Annotation"),
icon="annotation.png")
reader.read('annotationparam', instance=self.annotationparam)
self.annotationparam.update_annotation(self)
self.shape.deserialize(reader)
self.label.deserialize(reader)
def set_style(self, section, option):
self.shape.set_style(section, option)
#----QwtPlotItem API--------------------------------------------------------
def draw(self, painter, xMap, yMap, canvasRect):
self.shape.draw(painter, xMap, yMap, canvasRect)
if self.label.isVisible():
self.label.draw(painter, xMap, yMap, canvasRect)
#----Public API-------------------------------------------------------------
def create_shape(self):
"""Return the shape object associated to this annotated shape object"""
shape = self.SHAPE_CLASS(0, 0, 1, 1)
return shape
def create_label(self):
"""Return the label object associated to this annotated shape object"""
label_param = LabelParam(_("Label"), icon='label.png')
label_param.read_config(CONF, "plot", "shape/label")
label_param.anchor = self.LABEL_ANCHOR
return DataInfoLabel(label_param, [self])
def is_label_visible(self):
"""Return True if associated label is visible"""
return self.label.isVisible()
def set_label_visible(self, state):
"""Set the annotated shape's label visibility"""
self.label.setVisible(state)
def update_label(self):
"""Update the annotated shape's label contents"""
self.label.update_text()
def get_text(self):
"""
Return text associated to current shape
(see :py:class:`plotpy.label.ObjectInfo`)
"""
text = ""
title = self.title().text()
if title:
text += "<b>%s</b>" % title
subtitle = self.annotationparam.subtitle
if subtitle:
if text:
text += "<br>"
text += "<i>%s</i>" % subtitle
if self.area_computations_visible:
infos = self.get_infos()
if infos:
if text:
text += "<br>"
text += infos
return text
def x_to_str(self, x):
"""Convert x (float) to a string
(with associated unit and uncertainty)"""
param = self.annotationparam
if self.plot() is None:
return ''
else:
xunit = self.plot().get_axis_unit(self.xAxis())
fmt = param.format
if param.uncertainty:
fmt += " ± " + (fmt % (x*param.uncertainty))
if xunit is not None:
return (fmt+" "+xunit) % x
else:
return (fmt) % x
def y_to_str(self, y):
"""Convert y (float) to a string
(with associated unit and uncertainty)"""
param = self.annotationparam
if self.plot() is None:
return ''
else:
yunit = self.plot().get_axis_unit(self.yAxis())
fmt = param.format
if param.uncertainty:
fmt += " ± " + (fmt % (y*param.uncertainty))
if yunit is not None:
return (fmt+" "+yunit) % y
else:
return (fmt) % y
def get_center(self):
"""Return shape center coordinates: (xc, yc)"""
return self.shape.get_center()
def get_tr_center(self):
"""Return shape center coordinates after applying transform matrix"""
raise NotImplementedError
def get_tr_center_str(self):
"""Return center coordinates as a string (with units)"""
xc, yc = self.get_tr_center()
return "( %s ; %s )" % (self.x_to_str(xc), self.y_to_str(yc))
def get_tr_size(self):
"""Return shape size after applying transform matrix"""
raise NotImplementedError
def get_tr_size_str(self):
"""Return size as a string (with units)"""
xs, ys = self.get_tr_size()
return "%s x %s" % (self.x_to_str(xs), self.y_to_str(ys))
def get_infos(self):
"""Return formatted string with informations on current shape"""
pass
def set_label_position(self):
"""Set label position, for instance based on shape position"""
raise NotImplementedError
def apply_transform_matrix(self, x, y):
V = np.array([x, y, 1.])
W = np.dot(V, self.annotationparam.transform_matrix)
return W[0], W[1]
def get_transformed_coords(self, handle1, handle2):
x1, y1 = self.apply_transform_matrix(*self.shape.points[handle1])
x2, y2 = self.apply_transform_matrix(*self.shape.points[handle2])
return x1, y1, x2, y2
#----IBasePlotItem API------------------------------------------------------
def hit_test(self, pos):
return self.shape.poly_hit_test(self.plot(),
self.xAxis(), self.yAxis(), pos)
def move_point_to(self, handle, pos, ctrl=None):
self.shape.move_point_to(handle, pos, ctrl)
self.set_label_position()
if self.plot():
self.plot().SIG_ANNOTATION_CHANGED.emit(self)
def move_shape(self, old_pos, new_pos):
self.shape.move_shape(old_pos, new_pos)
self.label.move_local_shape(old_pos, new_pos)
def move_local_shape(self, old_pos, new_pos):
old_pt = canvas_to_axes(self, old_pos)
new_pt = canvas_to_axes(self, new_pos)
self.shape.move_shape(old_pt, new_pt)
self.set_label_position()
if self.plot():
self.plot().SIG_ITEM_MOVED.emit(self, *(old_pt+new_pt))
self.plot().SIG_ANNOTATION_CHANGED.emit(self)
def move_with_selection(self, delta_x, delta_y):
"""
Translate the shape together with other selected items
delta_x, delta_y: translation in plot coordinates
"""
self.shape.move_with_selection(delta_x, delta_y)
self.label.move_with_selection(delta_x, delta_y)
self.plot().SIG_ANNOTATION_CHANGED.emit(self)
def select(self):
"""Select item"""
AbstractShape.select(self)
self.shape.select()
def unselect(self):
"""Unselect item"""
AbstractShape.unselect(self)
self.shape.unselect()
def get_item_parameters(self, itemparams):
self.shape.get_item_parameters(itemparams)
self.label.get_item_parameters(itemparams)
self.annotationparam.update_param(self)
itemparams.add("AnnotationParam", self, self.annotationparam)
def set_item_parameters(self, itemparams):
self.shape.set_item_parameters(itemparams)
self.label.set_item_parameters(itemparams)
update_dataset(self.annotationparam, itemparams.get("AnnotationParam"),
visible_only=True)
self.annotationparam.update_annotation(self)
assert_interfaces_valid(AnnotatedShape)
[docs]class AnnotatedPoint(AnnotatedShape):
"""
Construct an annotated point at coordinates (x, y)
with properties set with *annotationparam*
(see :py:class:`plotpy.styles.AnnotationParam`)
"""
SHAPE_CLASS = PointShape
LABEL_ANCHOR = "TL"
def __init__(self, x=0, y=0, annotationparam=None):
AnnotatedShape.__init__(self, annotationparam)
self.set_pos(x, y)
#----Public API-------------------------------------------------------------
[docs] def set_pos(self, x, y):
"""Set the point coordinates to (x, y)"""
self.shape.set_pos(x, y)
self.set_label_position()
[docs] def get_pos(self):
"""Return the point coordinates"""
return self.shape.get_pos()
#----AnnotatedShape API-----------------------------------------------------
[docs] def create_shape(self):
"""Return the shape object associated to this annotated shape object"""
shape = self.SHAPE_CLASS(0, 0)
return shape
[docs] def set_label_position(self):
"""Set label position, for instance based on shape position"""
x, y = self.shape.points[0]
self.label.set_pos(x, y)
#----AnnotatedShape API-----------------------------------------------------
[docs] def get_infos(self):
"""Return formatted string with informations on current shape"""
xt, yt = self.apply_transform_matrix(*self.shape.points[0])
return "( %s ; %s )" % (self.x_to_str(xt), self.y_to_str(yt))
[docs]class AnnotatedSegment(AnnotatedShape):
"""
Construct an annotated segment between coordinates (x1, y1) and
(x2, y2) with properties set with *annotationparam*
(see :py:class:`plotpy.styles.AnnotationParam`)
"""
SHAPE_CLASS = SegmentShape
LABEL_ANCHOR = "C"
def __init__(self, x1=0, y1=0, x2=0, y2=0, annotationparam=None):
AnnotatedShape.__init__(self, annotationparam)
self.set_rect(x1, y1, x2, y2)
#----Public API-------------------------------------------------------------
[docs] def set_rect(self, x1, y1, x2, y2):
"""
Set the coordinates of the shape's top-left corner to (x1, y1),
and of its bottom-right corner to (x2, y2).
"""
self.shape.set_rect(x1, y1, x2, y2)
self.set_label_position()
[docs] def get_rect(self):
"""
Return the coordinates of the shape's top-left and bottom-right corners
"""
return self.shape.get_rect()
[docs] def get_tr_length(self):
"""Return segment length after applying transform matrix"""
return compute_distance(*self.get_transformed_coords(0, 1))
#----AnnotatedShape API-----------------------------------------------------
[docs] def set_label_position(self):
"""Set label position, for instance based on shape position"""
x1, y1, x2, y2 = self.get_rect()
self.label.set_pos(*compute_center(x1, y1, x2, y2))
#----AnnotatedShape API-----------------------------------------------------
[docs] def get_infos(self):
"""Return formatted string with informations on current shape"""
return _("Distance:") + " " + self.x_to_str(self.get_tr_length())
[docs]class AnnotatedRectangle(AnnotatedShape):
"""
Construct an annotated rectangle between coordinates (x1, y1) and
(x2, y2) with properties set with *annotationparam*
(see :py:class:`plotpy.styles.AnnotationParam`)
"""
SHAPE_CLASS = RectangleShape
LABEL_ANCHOR = "TL"
def __init__(self, x1=0, y1=0, x2=0, y2=0, annotationparam=None):
AnnotatedShape.__init__(self, annotationparam)
self.set_rect(x1, y1, x2, y2)
#----Public API-------------------------------------------------------------
[docs] def set_rect(self, x1, y1, x2, y2):
"""
Set the coordinates of the shape's top-left corner to (x1, y1),
and of its bottom-right corner to (x2, y2).
"""
self.shape.set_rect(x1, y1, x2, y2)
self.set_label_position()
[docs] def get_rect(self):
"""
Return the coordinates of the shape's top-left and bottom-right corners
"""
return self.shape.get_rect()
#----AnnotatedShape API-----------------------------------------------------
[docs] def set_label_position(self):
"""Set label position, for instance based on shape position"""
x_label, y_label = self.shape.points.min(axis=0)
self.label.set_pos(x_label, y_label)
[docs] def get_computations_text(self):
"""Return formatted string with informations on current shape"""
tdict = self.get_string_dict()
return "%(center_n)s ( %(center)s )<br>%(size_n)s %(size)s" % tdict
[docs] def get_tr_center(self):
"""Return shape center coordinates after applying transform matrix"""
return compute_center(*self.get_transformed_coords(0, 2))
[docs] def get_tr_size(self):
"""Return shape size after applying transform matrix"""
return compute_rect_size(*self.get_transformed_coords(0, 2))
[docs] def get_infos(self):
"""Return formatted string with informations on current shape"""
return "<br>".join([
_("Center:") + " " + self.get_tr_center_str(),
_("Size:") + " " + self.get_tr_size_str(),
])
[docs]class AnnotatedObliqueRectangle(AnnotatedRectangle):
"""
Construct an annotated oblique rectangle between coordinates (x0, y0),
(x1, y1), (x2, y2) and (x3, y3) with properties set with *annotationparam*
(see :py:class:`plotpy.styles.AnnotationParam`)
"""
SHAPE_CLASS = ObliqueRectangleShape
LABEL_ANCHOR = "C"
def __init__(self, x0=0, y0=0, x1=0, y1=0, x2=0, y2=0, x3=0, y3=0,
annotationparam=None):
AnnotatedShape.__init__(self, annotationparam)
self.set_rect(x0, y0, x1, y1, x2, y2, x3, y3)
#----Public API-------------------------------------------------------------
[docs] def get_tr_angle(self):
"""Return X-diameter angle with horizontal direction,
after applying transform matrix"""
xcoords = self.get_transformed_coords(0, 1)
_x, yr1 = self.apply_transform_matrix(1., 1.)
_x, yr2 = self.apply_transform_matrix(1., 2.)
return (compute_angle(reverse=yr1 > yr2, *xcoords)+90)%180-90
[docs] def get_bounding_rect_coords(self):
"""Return bounding rectangle coordinates (in plot coordinates)"""
return self.shape.get_bounding_rect_coords()
#----AnnotatedShape API-----------------------------------------------------
[docs] def create_shape(self):
"""Return the shape object associated to this annotated shape object"""
shape = self.SHAPE_CLASS(0, 0, 0, 0, 0, 0, 0, 0)
return shape
#----AnnotatedShape API-----------------------------------------------------
[docs] def set_label_position(self):
"""Set label position, for instance based on shape position"""
self.label.set_pos(*self.get_center())
#----RectangleShape API-----------------------------------------------------
[docs] def set_rect(self, x0, y0, x1, y1, x2, y2, x3, y3):
"""
Set the rectangle corners coordinates:
(x0, y0): top-left corner
(x1, y1): top-right corner
(x2, y2): bottom-right corner
(x3, y3): bottom-left corner
::
x: additionnal points
(x0, y0)------>(x1, y1)
↑ |
| |
x x
| |
| ↓
(x3, y3)<------(x2, y2)
"""
self.shape.set_rect(x0, y0, x1, y1, x2, y2, x3, y3)
self.set_label_position()
[docs] def get_tr_size(self):
"""Return shape size after applying transform matrix"""
dx = compute_distance(*self.get_transformed_coords(0, 1))
dy = compute_distance(*self.get_transformed_coords(0, 3))
return dx, dy
#----AnnotatedShape API-----------------------------------------------------
[docs] def get_infos(self):
"""Return formatted string with informations on current shape"""
return "<br>".join([
_("Center:") + " " + self.get_tr_center_str(),
_("Size:") + " " + self.get_tr_size_str(),
_("Angle:") + " %.1f°" % self.get_tr_angle(),
])
[docs]class AnnotatedEllipse(AnnotatedShape):
"""
Construct an annotated ellipse with X-axis diameter between
coordinates (x1, y1) and (x2, y2)
and properties set with *annotationparam*
(see :py:class:`plotpy.styles.AnnotationParam`)
"""
SHAPE_CLASS = EllipseShape
LABEL_ANCHOR = "C"
def __init__(self, x1=0, y1=0, x2=0, y2=0, annotationparam=None):
AnnotatedShape.__init__(self, annotationparam)
self.set_xdiameter(x1, y1, x2, y2)
#----Public API-------------------------------------------------------------
[docs] def set_xdiameter(self, x0, y0, x1, y1):
"""Set the coordinates of the ellipse's X-axis diameter
Warning: transform matrix is not applied here"""
self.shape.set_xdiameter(x0, y0, x1, y1)
self.set_label_position()
[docs] def get_xdiameter(self):
"""Return the coordinates of the ellipse's X-axis diameter
Warning: transform matrix is not applied here"""
return self.shape.get_xdiameter()
[docs] def set_ydiameter(self, x2, y2, x3, y3):
"""Set the coordinates of the ellipse's Y-axis diameter
Warning: transform matrix is not applied here"""
self.shape.set_ydiameter(x2, y2, x3, y3)
self.set_label_position()
[docs] def get_ydiameter(self):
"""Return the coordinates of the ellipse's Y-axis diameter
Warning: transform matrix is not applied here"""
return self.shape.get_ydiameter()
def get_rect(self):
return self.shape.get_rect()
def set_rect(self, x0, y0, x1, y1):
raise NotImplementedError
[docs] def get_tr_angle(self):
"""Return X-diameter angle with horizontal direction,
after applying transform matrix"""
xcoords = self.get_transformed_coords(0, 1)
_x, yr1 = self.apply_transform_matrix(1., 1.)
_x, yr2 = self.apply_transform_matrix(1., 2.)
return (compute_angle(reverse=yr1 > yr2, *xcoords)+90)%180-90
#----AnnotatedShape API-----------------------------------------------------
[docs] def set_label_position(self):
"""Set label position, for instance based on shape position"""
x_label, y_label = self.shape.points.mean(axis=0)
self.label.set_pos(x_label, y_label)
[docs] def get_tr_center(self):
"""Return center coordinates: (xc, yc)"""
return compute_center(*self.get_transformed_coords(0, 1))
[docs] def get_tr_size(self):
"""Return shape size after applying transform matrix"""
xcoords = self.get_transformed_coords(0, 1)
ycoords = self.get_transformed_coords(2, 3)
dx = compute_distance(*xcoords)
dy = compute_distance(*ycoords)
if np.fabs(self.get_tr_angle()) > 45:
dx, dy = dy, dx
return dx, dy
[docs] def get_infos(self):
"""Return formatted string with informations on current shape"""
return "<br>".join([
_("Center:") + " " + self.get_tr_center_str(),
_("Size:") + " " + self.get_tr_size_str(),
_("Angle:") + " %.1f°" % self.get_tr_angle(),
])
[docs]class AnnotatedCircle(AnnotatedEllipse):
"""
Construct an annotated circle with diameter between coordinates
(x1, y1) and (x2, y2) and properties set with *annotationparam*
(see :py:class:`plotpy.styles.AnnotationParam`)
"""
def __init__(self, x1=0, y1=0, x2=0, y2=0, annotationparam=None):
AnnotatedEllipse.__init__(self, x1, y1, x2, y2, annotationparam)
[docs] def get_tr_diameter(self):
"""Return circle diameter after applying transform matrix"""
return compute_distance(*self.get_transformed_coords(0, 1))
#----AnnotatedShape API-------------------------------------------------
[docs] def get_infos(self):
"""Return formatted string with informations on current shape"""
return "<br>".join([
_("Center:")+" "+self.get_tr_center_str(),
_("Diameter:")+" "+self.x_to_str(self.get_tr_diameter()),
])
#----AnnotatedEllipse API---------------------------------------------------
def set_rect(self, x0, y0, x1, y1):
self.shape.set_rect(x0, y0, x1, y1)