# -*- coding: utf-8 -*-
#
# Copyright © 2009-2010 CEA
# Pierre Raybaut
# Licensed under the terms of the CECILL License
# (see guiqwt/__init__.py for details)
# pylint: disable=C0103
"""
guiqwt.curve
------------
The `curve` module provides curve-related objects:
* :py:class:`guiqwt.curve.CurvePlot`: a 2d curve plotting widget
* :py:class:`guiqwt.curve.CurveItem`: a curve plot item
* :py:class:`guiqwt.curve.ErrorBarCurveItem`: a curve plot item with
error bars
* :py:class:`guiqwt.curve.GridItem`
* :py:class:`guiqwt.curve.ItemListWidget`: base widget implementing the
`plot item list panel`
* :py:class:`guiqwt.curve.PlotItemList`: the `plot item list panel`
``CurveItem`` and ``GridItem`` objects are plot items (derived from
QwtPlotItem) that may be displayed on a 2D plotting widget like
:py:class:`guiqwt.curve.CurvePlot` or :py:class:`guiqwt.image.ImagePlot`.
.. seealso::
Module :py:mod:`guiqwt.image`
Module providing image-related plot items and plotting widgets
Module :py:mod:`guiqwt.plot`
Module providing ready-to-use curve and image plotting widgets and
dialog boxes
Examples
~~~~~~~~
Create a basic curve plotting widget:
* before creating any widget, a `QApplication` must be instantiated (that
is a `Qt` internal requirement):
>>> import guidata
>>> app = guidata.qapplication()
* that is mostly equivalent to the following (the only difference is that
the `guidata` helper function also installs the `Qt` translation
corresponding to the system locale):
>>> from PyQt5.QtGui import QApplication
>>> app = QApplication([])
* now that a `QApplication` object exists, we may create the plotting
widget:
>>> from guiqwt.curve import CurvePlot
>>> plot = CurvePlot(title="Example", xlabel="X", ylabel="Y")
Create a curve item:
* from the associated plot item class (e.g. `ErrorBarCurveItem` to
create a curve with error bars): the item properties are then assigned
by creating the appropriate style parameters object
(e.g. :py:class:`guiqwt.styles.ErrorBarParam`)
>>> from guiqwt.curve import CurveItem
>>> from guiqwt.styles import CurveParam
>>> param = CurveParam()
>>> param.label = 'My curve'
>>> curve = CurveItem(param)
>>> curve.set_data(x, y)
* or using the `plot item builder` (see :py:func:`guiqwt.builder.make`):
>>> from guiqwt.builder import make
>>> curve = make.curve(x, y, title='My curve')
Attach the curve to the plotting widget:
>>> plot.add_item(curve)
Display the plotting widget:
>>> plot.show()
>>> app.exec_()
Reference
~~~~~~~~~
.. autoclass:: CurvePlot
:members:
:inherited-members:
.. autoclass:: CurveItem
:members:
:inherited-members:
.. autoclass:: ErrorBarCurveItem
:members:
:inherited-members:
.. autoclass:: PlotItemList
:members:
"""
import warnings
import numpy as np
import sys
from qtpy.QtWidgets import (
QMenu,
QListWidget,
QListWidgetItem,
QVBoxLayout,
QToolBar,
QMessageBox,
)
from qtpy.QtGui import QBrush, QColor, QPen, QPolygonF
from qtpy.QtCore import Qt, QPointF, QLineF, QRectF, Signal
from guidata.utils import assert_interfaces_valid, update_dataset
from guidata.configtools import get_icon, get_image_layout
from guidata.qthelpers import create_action, add_actions
# Local imports
from guiqwt.transitional import QwtPlotCurve, QwtPlotGrid, QwtPlotItem, QwtScaleMap
from guiqwt.config import CONF, _
from guiqwt.interfaces import (
IBasePlotItem,
IDecoratorItemType,
ISerializableType,
ICurveItemType,
ITrackableItemType,
IPanel,
)
from guiqwt.panels import PanelWidget, ID_ITEMLIST
from guiqwt.baseplot import BasePlot, canvas_to_axes
from guiqwt.styles import GridParam, CurveParam, ErrorBarParam, SymbolParam
from guiqwt.shapes import Marker
def _simplify_poly(pts, off, scale, bounds):
ax, bx, ay, by = scale
xm, ym, xM, yM = bounds
a = np.array([[ax, ay]])
b = np.array([[bx, by]])
_pts = a * pts + b
poly = []
NP = off.shape[0]
for i in range(off.shape[0]):
i0 = off[i, 1]
if i + 1 < NP:
i1 = off[i + 1, 1]
else:
i1 = pts.shape[0]
poly.append((_pts[i0:i1], i))
return poly
try:
from gshhs import simplify_poly
except ImportError:
simplify_poly = _simplify_poly
def seg_dist(P, P0, P1):
"""
Return distance between point P and segment (P0, P1)
If P orthogonal projection on (P0, P1) is outside segment bounds, return
either distance to P0 or to P1 (the closest one)
P, P0, P1: QPointF instances
"""
u = QLineF(P0, P).length()
if P0 == P1:
return u
else:
angle = QLineF(P0, P).angleTo(QLineF(P0, P1)) * np.pi / 180
projection = u * np.cos(angle)
if projection > QLineF(P0, P1).length():
return QLineF(P1, P).length()
elif projection < 0:
return QLineF(P0, P).length()
else:
return abs(u * np.sin(angle))
def test_seg_dist():
print(seg_dist(QPointF(200, 100), QPointF(150, 196), QPointF(250, 180)))
print(seg_dist(QPointF(200, 100), QPointF(190, 196), QPointF(210, 180)))
print(seg_dist(QPointF(201, 105), QPointF(201, 196), QPointF(201, 180)))
def norm2(v):
return (v**2).sum(axis=1)
def seg_dist_v(P, X0, Y0, X1, Y1):
"""Version vectorielle de seg_dist"""
V = np.zeros((X0.shape[0], 2), float)
PP = np.zeros((X0.shape[0], 2), float)
PP[:, 0] = X0
PP[:, 1] = Y0
V[:, 0] = X1 - X0
V[:, 1] = Y1 - Y0
dP = np.array(P).reshape(1, 2) - PP
nV = np.sqrt(norm2(V)).clip(1e-12) # clip: avoid division by zero
w2 = V / nV[:, np.newaxis]
w = np.array([-w2[:, 1], w2[:, 0]]).T
distances = np.fabs((dP * w).sum(axis=1))
ix = distances.argmin()
return ix, distances[ix]
def test_seg_dist_v():
"""Test de seg_dist_v"""
a = (np.arange(10.0) ** 2).reshape(5, 2)
ix, dist = seg_dist_v((2.1, 3.3), a[:-1, 0], a[:-1, 1], a[1:, 0], a[1:, 1])
print(ix, dist)
assert ix == 0
if __name__ == "__main__":
test_seg_dist_v()
test_seg_dist()
SELECTED_SYMBOL_PARAM = SymbolParam()
SELECTED_SYMBOL_PARAM.read_config(CONF, "plot", "selected_curve_symbol")
SELECTED_SYMBOL = SELECTED_SYMBOL_PARAM.build_symbol()
class GridItem(QwtPlotGrid):
"""
Construct a grid `plot item` with the parameters *gridparam*
(see :py:class:`guiqwt.styles.GridParam`)
"""
__implements__ = (IBasePlotItem,)
_readonly = True
_private = False
def __init__(self, gridparam=None):
super(GridItem, self).__init__()
if gridparam is None:
self.gridparam = GridParam(title=_("Grid"), icon="grid.png")
else:
self.gridparam = gridparam
self.selected = False
self.immutable = True # set to false to allow moving points around
self.update_params() # won't work completely because it's not yet
# attached to plot (actually, only canvas background won't be updated)
def types(self):
return (IDecoratorItemType,)
def attach(self, plot):
"""Reimplemented to update plot canvas background"""
QwtPlotGrid.attach(self, plot)
self.update_params()
def set_readonly(self, state):
"""Set object read-only state"""
self._readonly = state
def is_readonly(self):
"""Return object read-only state"""
return self._readonly
def set_private(self, state):
"""Set object as private"""
self._private = state
def is_private(self):
"""Return True if object is private"""
return self._private
def set_selectable(self, state):
"""Set item selectable state"""
self._can_select = state
def set_resizable(self, state):
"""Set item resizable state
(or any action triggered when moving an handle, e.g. rotation)"""
self._can_resize = state
def set_movable(self, state):
"""Set item movable state"""
self._can_move = state
def set_rotatable(self, state):
"""Set item rotatable state"""
self._can_rotate = state
def can_select(self):
return False
def can_resize(self):
return False
def can_rotate(self):
return False
def can_move(self):
return False
def select(self):
"""Select item"""
pass
def unselect(self):
"""Unselect item"""
pass
def hit_test(self, pos):
return sys.maxsize, 0, False, None
def move_local_point_to(self, handle, pos, ctrl=None):
pass
def move_local_shape(self, old_pos, new_pos):
pass
def move_with_selection(self, delta_x, delta_y):
pass
def update_params(self):
self.gridparam.update_grid(self)
def update_item_parameters(self):
self.gridparam.update_param(self)
def get_item_parameters(self, itemparams):
itemparams.add("GridParam", self, self.gridparam)
def set_item_parameters(self, itemparams):
self.gridparam = itemparams.get("GridParam")
self.gridparam.update_grid(self)
assert_interfaces_valid(GridItem)
[docs]class CurveItem(QwtPlotCurve):
"""
Construct a curve `plot item` with the parameters *curveparam*
(see :py:class:`guiqwt.styles.CurveParam`)
"""
__implements__ = (IBasePlotItem, ISerializableType)
_readonly = False
_private = False
def __init__(self, curveparam=None):
super(CurveItem, self).__init__()
if curveparam is None:
self.curveparam = CurveParam(_("Curve"), icon="curve.png")
else:
self.curveparam = curveparam
self.selected = False
self.immutable = True # set to false to allow moving points around
self._x = None
self._y = None
self.update_params()
def _get_visible_axis_min(self, axis_id, axis_data):
"""Return axis minimum excluding zero and negative values when
corresponding plot axis scale is logarithmic"""
if self.plot().get_axis_scale(axis_id) == "log":
return axis_data[axis_data > 0].min()
else:
return axis_data.min()
[docs] def boundingRect(self):
"""Return the bounding rectangle of the data"""
plot = self.plot()
if plot is not None and "log" in (
plot.get_axis_scale(self.xAxis()),
plot.get_axis_scale(self.yAxis()),
):
x, y = self._x, self._y
xf, yf = x[np.isfinite(x)], y[np.isfinite(y)]
xmin = self._get_visible_axis_min(self.xAxis(), xf)
ymin = self._get_visible_axis_min(self.yAxis(), yf)
return QRectF(xmin, ymin, xf.max() - xmin, yf.max() - ymin)
else:
return QwtPlotCurve.boundingRect(self)
def types(self):
return (ICurveItemType, ITrackableItemType, ISerializableType)
[docs] def set_selectable(self, state):
"""Set item selectable state"""
self._can_select = state
[docs] def set_resizable(self, state):
"""Set item resizable state
(or any action triggered when moving an handle, e.g. rotation)"""
self._can_resize = state
[docs] def set_movable(self, state):
"""Set item movable state"""
self._can_move = state
[docs] def set_rotatable(self, state):
"""Set item rotatable state"""
self._can_rotate = state
def can_select(self):
return True
def can_resize(self):
return False
def can_rotate(self):
return False
def can_move(self):
return False
def __reduce__(self):
state = (self.curveparam, self._x, self._y, self.z())
res = (CurveItem, (), state)
return res
def __setstate__(self, state):
param, x, y, z = state
self.curveparam = param
self.set_data(x, y)
self.setZ(z)
self.update_params()
[docs] def serialize(self, writer):
"""Serialize object to HDF5 writer"""
writer.write(self._x, group_name="Xdata")
writer.write(self._y, group_name="Ydata")
writer.write(self.z(), group_name="z")
self.curveparam.update_param(self)
writer.write(self.curveparam, group_name="curveparam")
[docs] def deserialize(self, reader):
"""Deserialize object from HDF5 reader"""
self.curveparam = CurveParam(_("Curve"), icon="curve.png")
reader.read("curveparam", instance=self.curveparam)
x = reader.read(group_name="Xdata", func=reader.read_array)
y = reader.read(group_name="Ydata", func=reader.read_array)
self.set_data(x, y)
self.setZ(reader.read("z"))
self.update_params()
[docs] def set_readonly(self, state):
"""Set object readonly state"""
self._readonly = state
[docs] def is_readonly(self):
"""Return object readonly state"""
return self._readonly
[docs] def set_private(self, state):
"""Set object as private"""
self._private = state
[docs] def is_private(self):
"""Return True if object is private"""
return self._private
def invalidate_plot(self):
plot = self.plot()
if plot is not None:
plot.invalidate()
[docs] def select(self):
"""Select item"""
self.selected = True
plot = self.plot()
if plot is not None:
plot.blockSignals(True)
self.setSymbol(SELECTED_SYMBOL)
if plot is not None:
plot.blockSignals(False)
self.invalidate_plot()
[docs] def unselect(self):
"""Unselect item"""
self.selected = False
# Restoring initial curve parameters:
self.curveparam.update_curve(self)
self.invalidate_plot()
[docs] def get_data(self):
"""Return curve data x, y (NumPy arrays)"""
return self._x, self._y
[docs] def set_data(self, x, y):
"""
Set curve data:
* x: NumPy array
* y: NumPy array
"""
self._x = np.array(x, copy=False)
self._y = np.array(y, copy=False)
self.setData(self._x, self._y)
[docs] def is_empty(self):
"""Return True if item data is empty"""
return self._x is None or self._y is None or self._y.size == 0
[docs] def hit_test(self, pos):
"""Calcul de la distance d'un point à une courbe
renvoie (dist, handle, inside)"""
if self.is_empty():
return sys.maxsize, 0, False, None
plot = self.plot()
ax = self.xAxis()
ay = self.yAxis()
px = plot.invTransform(ax, pos.x())
py = plot.invTransform(ay, pos.y())
# On cherche les 4 points qui sont les plus proches en X et en Y
# avant et après ie tels que p1x < x < p2x et p3y < y < p4y
tmpx = self._x - px
tmpy = self._y - py
if np.count_nonzero(tmpx) != len(tmpx) or np.count_nonzero(tmpy) != len(tmpy):
# Avoid dividing by zero warning when computing dx or dy
return sys.maxsize, 0, False, None
dx = 1 / tmpx
dy = 1 / tmpy
i0 = dx.argmin()
i1 = dx.argmax()
i2 = dy.argmin()
i3 = dy.argmax()
t = np.array((i0, i1, i2, i3))
t2 = (t + 1).clip(0, self._x.shape[0] - 1)
i, _d = seg_dist_v((px, py), self._x[t], self._y[t], self._x[t2], self._y[t2])
i = t[i]
# Recalcule la distance dans le répère du widget
p0x = plot.transform(ax, self._x[i])
p0y = plot.transform(ay, self._y[i])
if i + 1 >= self._x.shape[0]:
p1x = p0x
p1y = p0y
else:
p1x = plot.transform(ax, self._x[i + 1])
p1y = plot.transform(ay, self._y[i + 1])
distance = seg_dist(QPointF(pos), QPointF(p0x, p0y), QPointF(p1x, p1y))
return distance, i, False, None
[docs] def get_closest_coordinates(self, x, y):
"""Renvoie les coordonnées (x',y') du point le plus proche de (x,y)
Méthode surchargée pour ErrorBarSignalCurve pour renvoyer
les coordonnées des pointes des barres d'erreur"""
plot = self.plot()
ax = self.xAxis()
ay = self.yAxis()
xc = plot.transform(ax, x)
yc = plot.transform(ay, y)
_distance, i, _inside, _other = self.hit_test(QPointF(xc, yc))
point = self.sample(i)
return point.x(), point.y()
def get_coordinates_label(self, xc, yc):
title = self.title().text()
return "%s:<br>x = %g<br>y = %g" % (title, xc, yc)
def get_closest_x(self, xc):
# We assume X is sorted, otherwise we'd need :
# argmin(abs(x-xc))
i = self._x.searchsorted(xc)
if i > 0:
if np.fabs(self._x[i - 1] - xc) < np.fabs(self._x[i] - xc):
return self._x[i - 1], self._y[i - 1]
return self._x[i], self._y[i]
def move_local_point_to(self, handle, pos, ctrl=None):
if self.immutable:
return
if handle < 0 or handle > self._x.shape[0]:
return
x, y = canvas_to_axes(self, pos)
self._x[handle] = x
self._y[handle] = y
self.setData(self._x, self._y)
self.plot().replot()
[docs] def move_local_shape(self, old_pos, new_pos):
"""Translate the shape such that old_pos becomes new_pos
in canvas coordinates"""
nx, ny = canvas_to_axes(self, new_pos)
ox, oy = canvas_to_axes(self, old_pos)
self._x += nx - ox
self._y += ny - oy
self.setData(self._x, self._y)
[docs] 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._x += delta_x
self._y += delta_y
self.setData(self._x, self._y)
def update_params(self):
self.curveparam.update_curve(self)
if self.selected:
self.select()
def update_item_parameters(self):
self.curveparam.update_param(self)
def get_item_parameters(self, itemparams):
itemparams.add("CurveParam", self, self.curveparam)
def set_item_parameters(self, itemparams):
update_dataset(self.curveparam, itemparams.get("CurveParam"), visible_only=True)
self.update_params()
assert_interfaces_valid(CurveItem)
class PolygonMapItem(QwtPlotItem):
"""
Construct a curve `plot item` with the parameters *curveparam*
(see :py:class:`guiqwt.styles.CurveParam`)
"""
__implements__ = (IBasePlotItem, ISerializableType)
_readonly = False
_private = False
_can_select = False
_can_resize = False
_can_move = False
_can_rotate = False
def __init__(self, curveparam=None):
super(PolygonMapItem, self).__init__()
if curveparam is None:
self.curveparam = CurveParam(_("PolygonMap"), icon="curve.png")
else:
self.curveparam = curveparam
self.selected = False
self.immutable = True # set to false to allow moving points around
self._pts = None # Array of points Mx2
self._n = None # Array of polygon offsets/ends Nx1 (polygon k points are _pts[_n[k-1]:_n[k]])
self._c = None # Color of polygon Nx2 [border,background] as RGBA uint32
self.update_params()
def types(self):
return (ICurveItemType, ITrackableItemType, ISerializableType)
def can_select(self):
return self._can_select
def can_resize(self):
return self._can_resize
def can_rotate(self):
return self._can_rotate
def can_move(self):
return self._can_move
def set_selectable(self, state):
"""Set item selectable state"""
self._can_select = state
def set_resizable(self, state):
"""Set item resizable state
(or any action triggered when moving an handle, e.g. rotation)"""
self._can_resize = state
def set_movable(self, state):
"""Set item movable state"""
self._can_move = state
def set_rotatable(self, state):
"""Set item rotatable state"""
self._can_rotate = state
def setPen(self, x):
pass
def setBrush(self, x):
pass
def setSymbol(self, x):
pass
def setCurveAttribute(self, x, y):
pass
def setStyle(self, x):
pass
def setCurveType(self, x):
pass
def setBaseline(self, x):
pass
def __reduce__(self):
state = (self.curveparam, self._pts, self._n, self._c, self.z())
res = (PolygonMapItem, (), state)
return res
def __setstate__(self, state):
param, pts, n, c, z = state
self.curveparam = param
self.set_data(pts, n, c)
self.setZ(z)
self.update_params()
def serialize(self, writer):
"""Serialize object to HDF5 writer"""
writer.write(self._pts, group_name="Pdata")
writer.write(self._n, group_name="Ndata")
writer.write(self._c, group_name="Cdata")
writer.write(self.z(), group_name="z")
self.curveparam.update_param(self)
writer.write(self.curveparam, group_name="curveparam")
def deserialize(self, reader):
"""Deserialize object from HDF5 reader"""
pts = reader.read(group_name="Pdata", func=reader.read_array)
n = reader.read(group_name="Ndata", func=reader.read_array)
c = reader.read(group_name="Cdata", func=reader.read_array)
self.set_data(pts, n, c)
self.setZ(reader.read("z"))
self.curveparam = CurveParam(_("PolygonMap"), icon="curve.png")
reader.read("curveparam", instance=self.curveparam)
self.update_params()
def set_readonly(self, state):
"""Set object readonly state"""
self._readonly = state
def is_readonly(self):
"""Return object readonly state"""
return self._readonly
def set_private(self, state):
"""Set object as private"""
self._private = state
def is_private(self):
"""Return True if object is private"""
return self._private
def invalidate_plot(self):
plot = self.plot()
if plot is not None:
plot.invalidate()
def select(self):
"""Select item"""
self.selected = True
self.setSymbol(SELECTED_SYMBOL)
self.invalidate_plot()
def unselect(self):
"""Unselect item"""
self.selected = False
# Restoring initial curve parameters:
self.curveparam.update_curve(self)
self.invalidate_plot()
def get_data(self):
"""Return curve data x, y (NumPy arrays)"""
return self._pts, self._n, self._c
def set_data(self, pts, n, c):
"""
Set curve data:
* x: NumPy array
* y: NumPy array
"""
self._pts = np.array(pts, copy=False)
self._n = np.array(n, copy=False)
self._c = np.array(c, copy=False)
xmin, ymin = self._pts.min(axis=0)
xmax, ymax = self._pts.max(axis=0)
self.bounds = QRectF(xmin, ymin, xmax - xmin, ymax - ymin)
def is_empty(self):
"""Return True if item data is empty"""
return self._pts is None or self._pts.size == 0
def hit_test(self, pos):
"""Calcul de la distance d'un point à une courbe
renvoie (dist, handle, inside)"""
if self.is_empty():
return sys.maxsize, 0, False, None
plot = self.plot()
# TODO
return distance, i, False, None
def get_closest_coordinates(self, x, y):
"""Renvoie les coordonnées (x',y') du point le plus proche de (x,y)
Méthode surchargée pour ErrorBarSignalCurve pour renvoyer
les coordonnées des pointes des barres d'erreur"""
# TODO
return x, y
def get_coordinates_label(self, xc, yc):
title = self.title().text()
return "%s:<br>x = %f<br>y = %f" % (title, xc, yc)
def move_local_point_to(self, handle, pos, ctrl=None):
return
def move_local_shape(self, old_pos, new_pos):
pass
def move_with_selection(self, delta_x, delta_y):
pass
def update_params(self):
self.curveparam.update_curve(self)
if self.selected:
self.select()
def update_item_parameters(self):
self.curveparam.update_param(self)
def get_item_parameters(self, itemparams):
itemparams.add("CurveParam", self, self.curveparam)
def set_item_parameters(self, itemparams):
update_dataset(self.curveparam, itemparams.get("CurveParam"), visible_only=True)
self.update_params()
def draw(self, painter, xMap, yMap, canvasRect):
# from time import time
p1x = xMap.p1()
s1x = xMap.s1()
ax = (xMap.p2() - p1x) / (xMap.s2() - s1x)
p1y = yMap.p1()
s1y = yMap.s1()
ay = (yMap.p2() - p1y) / (yMap.s2() - s1y)
bx, by = p1x - s1x * ax, p1y - s1y * ay
_c = self._c
_n = self._n
fgcol = QColor()
bgcol = QColor()
# t0 = time()
polygons = simplify_poly(
self._pts, _n, (ax, bx, ay, by), canvasRect.getCoords()
)
# t1 = time()
# print len(polygons), t1-t0
# t2 = time()
for poly, num in polygons:
points = []
for i in range(poly.shape[0]):
points.append(QPointF(poly[i, 0], poly[i, 1]))
pg = QPolygonF(points)
fgcol.setRgba(int(_c[num, 0]))
bgcol.setRgba(int(_c[num, 1]))
painter.setPen(QPen(fgcol))
painter.setBrush(QBrush(bgcol))
painter.drawPolygon(pg)
# print "poly:", time()-t2
def boundingRect(self):
return self.bounds
assert_interfaces_valid(PolygonMapItem)
def _transform(map, v):
return QwtScaleMap.transform(map, v)
def vmap(map, v):
"""Transform coordinates while handling RuntimeWarning
that could be raised by NumPy when trying to transform
a zero in logarithmic scale for example"""
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=RuntimeWarning)
output = np.vectorize(_transform)(map, v)
return output
[docs]class ErrorBarCurveItem(CurveItem):
"""
Construct an error-bar curve `plot item`
with the parameters *errorbarparam*
(see :py:class:`guiqwt.styles.ErrorBarParam`)
"""
def __init__(self, curveparam=None, errorbarparam=None):
if errorbarparam is None:
self.errorbarparam = ErrorBarParam(_("Error bars"), icon="errorbar.png")
else:
self.errorbarparam = errorbarparam
super(ErrorBarCurveItem, self).__init__(curveparam)
self._dx = None
self._dy = None
self._minmaxarrays = {}
[docs] def serialize(self, writer):
"""Serialize object to HDF5 writer"""
super(ErrorBarCurveItem, self).serialize(writer)
writer.write(self._dx, group_name="dXdata")
writer.write(self._dy, group_name="dYdata")
self.errorbarparam.update_param(self)
writer.write(self.errorbarparam, group_name="errorbarparam")
[docs] def deserialize(self, reader):
"""Deserialize object from HDF5 reader"""
self.curveparam = CurveParam(_("Curve"), icon="curve.png")
reader.read("curveparam", instance=self.curveparam)
self.errorbarparam = ErrorBarParam(_("Error bars"), icon="errorbar.png")
reader.read("errorbarparam", instance=self.errorbarparam)
x = reader.read(group_name="Xdata", func=reader.read_array)
y = reader.read(group_name="Ydata", func=reader.read_array)
dx = reader.read(group_name="dXdata", func=reader.read_array)
dy = reader.read(group_name="dYdata", func=reader.read_array)
self.set_data(x, y, dx, dy)
self.setZ(reader.read("z"))
self.update_params()
[docs] def unselect(self):
"""Unselect item"""
CurveItem.unselect(self)
self.errorbarparam.update_curve(self)
[docs] def get_data(self):
"""
Return error-bar curve data: x, y, dx, dy
* x: NumPy array
* y: NumPy array
* dx: float or NumPy array (non-constant error bars)
* dy: float or NumPy array (non-constant error bars)
"""
return self._x, self._y, self._dx, self._dy
[docs] def set_data(self, x, y, dx=None, dy=None):
"""
Set error-bar curve data:
* x: NumPy array
* y: NumPy array
* dx: float or NumPy array (non-constant error bars)
* dy: float or NumPy array (non-constant error bars)
"""
CurveItem.set_data(self, x, y)
if dx is not None:
dx = np.array(dx, copy=False)
if dx.size == 0:
dx = None
if dy is not None:
dy = np.array(dy, copy=False)
if dy.size == 0:
dy = None
self._dx = dx
self._dy = dy
self._minmaxarrays = {}
def get_minmax_arrays(self, all_values=True):
if self._minmaxarrays.get(all_values) is None:
x = self._x
y = self._y
dx = self._dx
dy = self._dy
if all_values:
if dx is None:
xmin = xmax = x
else:
xmin, xmax = x - dx, x + dx
if dy is None:
ymin = ymax = y
else:
ymin, ymax = y - dy, y + dy
self._minmaxarrays.setdefault(all_values, (xmin, xmax, ymin, ymax))
else:
isf = np.logical_and(np.isfinite(x), np.isfinite(y))
if dx is not None:
isf = np.logical_and(isf, np.isfinite(dx))
if dy is not None:
isf = np.logical_and(isf, np.isfinite(dy))
if dx is None:
xmin = xmax = x[isf]
else:
xmin, xmax = x[isf] - dx[isf], x[isf] + dx[isf]
if dy is None:
ymin = ymax = y[isf]
else:
ymin, ymax = y[isf] - dy[isf], y[isf] + dy[isf]
self._minmaxarrays.setdefault(
all_values, (x[isf], y[isf], xmin, xmax, ymin, ymax)
)
return self._minmaxarrays[all_values]
[docs] def get_closest_coordinates(self, x, y):
# Surcharge d'une méthode de base de CurveItem
plot = self.plot()
ax = self.xAxis()
ay = self.yAxis()
xc = plot.transform(ax, x)
yc = plot.transform(ay, y)
_distance, i, _inside, _other = self.hit_test(QPointF(xc, yc))
x0, y0 = self.plot().canvas2plotitem(self, xc, yc)
x = self._x[i]
y = self._y[i]
xmin, xmax, ymin, ymax = self.get_minmax_arrays()
if abs(y0 - y) > abs(y0 - ymin[i]):
y = ymin[i]
elif abs(y0 - y) > abs(y0 - ymax[i]):
y = ymax[i]
if abs(x0 - x) > abs(x0 - xmin[i]):
x = xmin[i]
elif abs(x0 - x) > abs(x0 - xmax[i]):
x = xmax[i]
return x, y
[docs] def boundingRect(self):
"""Return the bounding rectangle of the data, error bars included"""
xmin, xmax, ymin, ymax = self.get_minmax_arrays()
if xmin is None or xmin.size == 0:
return CurveItem.boundingRect(self)
plot = self.plot()
xminf, yminf = xmin[np.isfinite(xmin)], ymin[np.isfinite(ymin)]
xmaxf, ymaxf = xmax[np.isfinite(xmax)], ymax[np.isfinite(ymax)]
if plot is not None and "log" in (
plot.get_axis_scale(self.xAxis()),
plot.get_axis_scale(self.yAxis()),
):
xmin = self._get_visible_axis_min(self.xAxis(), xminf)
ymin = self._get_visible_axis_min(self.yAxis(), yminf)
else:
xmin = xminf.min()
ymin = yminf.min()
return QRectF(xmin, ymin, xmaxf.max() - xmin, ymaxf.max() - ymin)
[docs] def draw(self, painter, xMap, yMap, canvasRect):
if self._x is None or self._x.size == 0:
return
x, y, xmin, xmax, ymin, ymax = self.get_minmax_arrays(all_values=False)
tx = vmap(xMap, x)
ty = vmap(yMap, y)
RN = list(range(len(tx)))
if self.errorOnTop:
QwtPlotCurve.draw(self, painter, xMap, yMap, canvasRect)
painter.save()
painter.setPen(self.errorPen)
cap = self.errorCap / 2.0
if self._dx is not None and self.errorbarparam.mode == 0:
txmin = vmap(xMap, xmin)
txmax = vmap(xMap, xmax)
# Classic error bars
lines = []
for i in RN:
yi = ty[i]
lines.append(QLineF(txmin[i], yi, txmax[i], yi))
painter.drawLines(lines)
if cap > 0:
lines = []
for i in RN:
yi = ty[i]
lines.append(QLineF(txmin[i], yi - cap, txmin[i], yi + cap))
lines.append(QLineF(txmax[i], yi - cap, txmax[i], yi + cap))
painter.drawLines(lines)
if self._dy is not None:
tymin = vmap(yMap, ymin)
tymax = vmap(yMap, ymax)
if self.errorbarparam.mode == 0:
# Classic error bars
lines = []
for i in RN:
xi = tx[i]
lines.append(QLineF(xi, tymin[i], xi, tymax[i]))
painter.drawLines(lines)
if cap > 0:
# Cap
lines = []
for i in RN:
xi = tx[i]
lines.append(QLineF(xi - cap, tymin[i], xi + cap, tymin[i]))
lines.append(QLineF(xi - cap, tymax[i], xi + cap, tymax[i]))
painter.drawLines(lines)
else:
# Error area
points = []
rpoints = []
for i in RN:
xi = tx[i]
points.append(QPointF(xi, tymin[i]))
rpoints.append(QPointF(xi, tymax[i]))
points += reversed(rpoints)
painter.setBrush(QBrush(self.errorBrush))
painter.drawPolygon(*points)
painter.restore()
if not self.errorOnTop:
QwtPlotCurve.draw(self, painter, xMap, yMap, canvasRect)
def update_params(self):
self.errorbarparam.update_curve(self)
CurveItem.update_params(self)
def update_item_parameters(self):
CurveItem.update_item_parameters(self)
self.errorbarparam.update_param(self)
def get_item_parameters(self, itemparams):
CurveItem.get_item_parameters(self, itemparams)
itemparams.add("ErrorBarParam", self, self.errorbarparam)
def set_item_parameters(self, itemparams):
update_dataset(
self.errorbarparam, itemparams.get("ErrorBarParam"), visible_only=True
)
CurveItem.set_item_parameters(self, itemparams)
assert_interfaces_valid(ErrorBarCurveItem)
# ===============================================================================
# Plot Widget
# ===============================================================================
class ItemListWidget(QListWidget):
"""
PlotItemList
List of items attached to plot
"""
def __init__(self, parent):
super(ItemListWidget, self).__init__(parent)
self.manager = None
self.plot = None # the default plot...
self.items = []
self.currentRowChanged.connect(self.current_row_changed)
self.itemChanged.connect(self.item_changed)
self.itemSelectionChanged.connect(self.refresh_actions)
self.itemSelectionChanged.connect(self.selection_changed)
self.setWordWrap(True)
self.setMinimumWidth(140)
self.setSelectionMode(QListWidget.ExtendedSelection)
# Setup context menu
self.menu = QMenu(self)
self.menu_actions = self.setup_actions()
self.refresh_actions()
add_actions(self.menu, self.menu_actions)
def register_panel(self, manager):
self.manager = manager
for plot in self.manager.get_plots():
plot.SIG_ITEMS_CHANGED.connect(self.items_changed)
plot.SIG_ACTIVE_ITEM_CHANGED.connect(self.items_changed)
self.plot = self.manager.get_plot()
def contextMenuEvent(self, event):
"""Override Qt method"""
self.refresh_actions()
self.menu.popup(event.globalPos())
def setup_actions(self):
self.movedown_ac = create_action(
self,
_("Move to back"),
icon=get_icon("arrow_down.png"),
triggered=lambda: self.move_item("down"),
)
self.moveup_ac = create_action(
self,
_("Move to front"),
icon=get_icon("arrow_up.png"),
triggered=lambda: self.move_item("up"),
)
settings_ac = create_action(
self,
_("Parameters..."),
icon=get_icon("settings.png"),
triggered=self.edit_plot_parameters,
)
self.remove_ac = create_action(
self, _("Remove"), icon=get_icon("trash.png"), triggered=self.remove_item
)
return [self.moveup_ac, self.movedown_ac, None, settings_ac, self.remove_ac]
def edit_plot_parameters(self):
self.plot.edit_plot_parameters("item")
def __is_selection_contiguous(self):
indexes = sorted([self.row(lw_item) for lw_item in self.selectedItems()])
return len(indexes) <= 1 or list(range(indexes[0], indexes[-1] + 1)) == indexes
def get_selected_items(self):
"""Return selected QwtPlot items
.. warning::
This is not the same as
:py:data:`guiqwt.baseplot.BasePlot.get_selected_items`.
Some items could appear in itemlist without being registered in
plot widget items (in particular, some items could be selected in
itemlist without being selected in plot widget)
"""
return [self.items[self.row(lw_item)] for lw_item in self.selectedItems()]
def refresh_actions(self):
is_selection = len(self.selectedItems()) > 0
for action in self.menu_actions:
if action is not None:
action.setEnabled(is_selection)
if is_selection:
remove_state = True
for item in self.get_selected_items():
remove_state = remove_state and not item.is_readonly()
self.remove_ac.setEnabled(remove_state)
for action in [self.moveup_ac, self.movedown_ac]:
action.setEnabled(self.__is_selection_contiguous())
def __get_item_icon(self, item):
from guiqwt.label import LegendBoxItem, LabelItem
from guiqwt.annotations import (
AnnotatedShape,
AnnotatedRectangle,
AnnotatedCircle,
AnnotatedEllipse,
AnnotatedPoint,
AnnotatedSegment,
)
from guiqwt.shapes import (
SegmentShape,
RectangleShape,
EllipseShape,
PointShape,
PolygonShape,
Axes,
XRangeSelection,
)
from guiqwt.image import BaseImageItem, Histogram2DItem, ImageFilterItem
from guiqwt.histogram import HistogramItem
icon_name = "item.png"
for klass, icon in (
(HistogramItem, "histogram.png"),
(ErrorBarCurveItem, "errorbar.png"),
(CurveItem, "curve.png"),
(GridItem, "grid.png"),
(LegendBoxItem, "legend.png"),
(LabelItem, "label.png"),
(AnnotatedSegment, "segment.png"),
(AnnotatedPoint, "point_shape.png"),
(AnnotatedCircle, "circle.png"),
(AnnotatedEllipse, "ellipse_shape.png"),
(AnnotatedRectangle, "rectangle.png"),
(AnnotatedShape, "annotation.png"),
(SegmentShape, "segment.png"),
(RectangleShape, "rectangle.png"),
(PointShape, "point_shape.png"),
(EllipseShape, "ellipse_shape.png"),
(Axes, "gtaxes.png"),
(Marker, "marker.png"),
(XRangeSelection, "xrange.png"),
(PolygonShape, "freeform.png"),
(Histogram2DItem, "histogram2d.png"),
(ImageFilterItem, "funct.png"),
(BaseImageItem, "image.png"),
):
if isinstance(item, klass):
icon_name = icon
break
return get_icon(icon_name)
def items_changed(self, plot):
"""Plot items have changed"""
active_plot = self.manager.get_active_plot()
if active_plot is not plot:
return
self.plot = plot
_block = self.blockSignals(True)
active = plot.get_active_item()
self.items = plot.get_public_items(z_sorted=True)
self.clear()
for item in self.items:
title = item.title().text()
lw_item = QListWidgetItem(self.__get_item_icon(item), title, self)
lw_item.setCheckState(Qt.Checked if item.isVisible() else Qt.Unchecked)
lw_item.setSelected(item.selected)
font = lw_item.font()
if item is active:
font.setItalic(True)
else:
font.setItalic(False)
lw_item.setFont(font)
self.addItem(lw_item)
self.refresh_actions()
self.blockSignals(_block)
def current_row_changed(self, index):
"""QListWidget current row has changed"""
if index == -1:
return
item = self.items[index]
if not item.can_select():
item = None
if item is None:
self.plot.replot()
def selection_changed(self):
items = self.get_selected_items()
self.plot.select_some_items(items)
self.plot.replot()
def item_changed(self, listwidgetitem):
"""QListWidget item has changed"""
item = self.items[self.row(listwidgetitem)]
visible = listwidgetitem.checkState() == Qt.Checked
if visible != item.isVisible():
self.plot.set_item_visible(item, visible)
def move_item(self, direction):
"""Move item to the background/foreground
Works only for contiguous selection
-> 'refresh_actions' method should guarantee that"""
items = self.get_selected_items()
if direction == "up":
self.plot.move_up(items)
else:
self.plot.move_down(items)
# Re-select items which can't be selected in plot widget but can be
# selected in ItemListWidget:
for item in items:
lw_item = self.item(self.items.index(item))
if not lw_item.isSelected():
lw_item.setSelected(True)
self.plot.replot()
def remove_item(self):
if len(self.selectedItems()) == 1:
message = _("Do you really want to remove this item?")
else:
message = _("Do you really want to remove selected items?")
answer = QMessageBox.warning(
self, _("Remove"), message, QMessageBox.Yes | QMessageBox.No
)
if answer == QMessageBox.Yes:
items = self.get_selected_items()
self.plot.del_items(items)
self.plot.replot()
[docs]class PlotItemList(PanelWidget):
"""Construct the `plot item list panel`"""
__implements__ = (IPanel,)
PANEL_ID = ID_ITEMLIST
PANEL_TITLE = _("Item list")
PANEL_ICON = "item_list.png"
def __init__(self, parent):
super(PlotItemList, self).__init__(parent)
self.manager = None
vlayout = QVBoxLayout()
self.setLayout(vlayout)
style = "<span style='color: #444444'><b>%s</b></span>"
layout, _label = get_image_layout(
self.PANEL_ICON, style % self.PANEL_TITLE, alignment=Qt.AlignCenter
)
vlayout.addLayout(layout)
self.listwidget = ItemListWidget(self)
vlayout.addWidget(self.listwidget)
toolbar = QToolBar(self)
vlayout.addWidget(toolbar)
add_actions(toolbar, self.listwidget.menu_actions)
[docs] def register_panel(self, manager):
"""Register panel to plot manager"""
self.manager = manager
self.listwidget.register_panel(manager)
assert_interfaces_valid(PlotItemList)
[docs]class CurvePlot(BasePlot):
"""
Construct a 2D curve plotting widget
(this class inherits :py:class:`guiqwt.baseplot.BasePlot`)
* parent: parent widget
* title: plot title
* xlabel: (bottom axis title, top axis title) or bottom axis title only
* ylabel: (left axis title, right axis title) or left axis title only
* xunit: (bottom axis unit, top axis unit) or bottom axis unit only
* yunit: (left axis unit, right axis unit) or left axis unit only
* gridparam: GridParam instance
* axes_synchronised: keep all x and y axes synchronised when zomming or
panning
"""
DEFAULT_ITEM_TYPE = ICurveItemType
AUTOSCALE_TYPES = (CurveItem, PolygonMapItem)
#: Signal emitted by plot when plot axis has changed, e.g. when panning/zooming (arg: plot))
SIG_PLOT_AXIS_CHANGED = Signal(object)
def __init__(
self,
parent=None,
title=None,
xlabel=None,
ylabel=None,
xunit=None,
yunit=None,
gridparam=None,
section="plot",
axes_synchronised=False,
):
super(CurvePlot, self).__init__(parent, section)
self.axes_reverse = [False] * 4
self.set_titles(
title=title, xlabel=xlabel, ylabel=ylabel, xunit=xunit, yunit=yunit
)
self.antialiased = False
self.set_antialiasing(CONF.get(section, "antialiasing"))
self.axes_synchronised = axes_synchronised
# Installing our own event filter:
# (qwt's event filter does not fit our needs)
self.canvas().installEventFilter(self.filter)
self.canvas().setMouseTracking(True)
self.cross_marker = Marker()
self.curve_marker = Marker(
label_cb=self.get_coordinates_str, constraint_cb=self.on_active_curve
)
self.__marker_stay_visible = False
self.cross_marker.set_style(section, "marker/cross")
self.curve_marker.set_style(section, "marker/curve")
self.cross_marker.setVisible(False)
self.curve_marker.setVisible(False)
self.cross_marker.attach(self)
self.curve_marker.attach(self)
self.curve_pointer = False
self.canvas_pointer = False
# Setting up grid
if gridparam is None:
gridparam = GridParam(title=_("Grid"), icon="grid.png")
gridparam.read_config(CONF, section, "grid")
self.grid = GridItem(gridparam)
self.add_item(self.grid, z=-1)
# ---- Private API ----------------------------------------------------------
def __del__(self):
# Sometimes, an obscure exception happens when we quit an application
# because if we don't remove the eventFilter it can still be called
# after the filter object has been destroyed by Python.
canvas = self.canvas()
if canvas:
try:
canvas.removeEventFilter(self.filter)
except RuntimeError:
# PySide2
pass
# generic helper methods
def canvas2plotitem(self, plot_item, x_canvas, y_canvas):
return (
self.invTransform(plot_item.xAxis(), x_canvas),
self.invTransform(plot_item.yAxis(), y_canvas),
)
def plotitem2canvas(self, plot_item, x, y):
return (
self.transform(plot_item.xAxis(), x),
self.transform(plot_item.yAxis(), y),
)
def on_active_curve(self, x, y):
curve = self.get_last_active_item(ITrackableItemType)
if curve:
x, y = curve.get_closest_coordinates(x, y)
return x, y
def get_coordinates_str(self, x, y):
title = _("Grid")
item = self.get_last_active_item(ITrackableItemType)
if item:
return item.get_coordinates_label(x, y)
return "<b>%s</b><br>x = %g<br>y = %g" % (title, x, y)
def set_marker_axes(self):
curve = self.get_last_active_item(ITrackableItemType)
if curve:
self.cross_marker.setAxes(curve.xAxis(), curve.yAxis())
self.curve_marker.setAxes(curve.xAxis(), curve.yAxis())
def do_move_marker(self, event):
pos = event.pos()
self.set_marker_axes()
if event.modifiers() & Qt.ShiftModifier or self.curve_pointer:
self.curve_marker.setZ(self.get_max_z() + 1)
self.cross_marker.setVisible(False)
self.curve_marker.setVisible(True)
self.curve_marker.move_local_point_to(0, pos)
self.replot()
self.__marker_stay_visible = event.modifiers() & Qt.ControlModifier
elif event.modifiers() & Qt.AltModifier or self.canvas_pointer:
self.cross_marker.setZ(self.get_max_z() + 1)
self.cross_marker.setVisible(True)
self.curve_marker.setVisible(False)
self.cross_marker.move_local_point_to(0, pos)
self.replot()
self.__marker_stay_visible = event.modifiers() & Qt.ControlModifier
else:
vis_cross = self.cross_marker.isVisible()
vis_curve = self.curve_marker.isVisible()
self.cross_marker.setVisible(False)
self.curve_marker.setVisible(self.__marker_stay_visible)
if vis_cross or vis_curve:
self.replot()
def get_axes_to_update(self, dx, dy):
if self.axes_synchronised:
axes = []
for axis_name in self.AXIS_NAMES:
if axis_name in ("left", "right"):
d = dy
else:
d = dx
axes.append((d, self.get_axis_id(axis_name)))
return axes
else:
xaxis, yaxis = self.get_active_axes()
return [(dx, xaxis), (dy, yaxis)]
[docs] def do_pan_view(self, dx, dy):
"""
Translate the active axes by dx, dy
dx, dy are tuples composed of (initial pos, dest pos)
"""
auto = self.autoReplot()
self.setAutoReplot(False)
axes_to_update = self.get_axes_to_update(dx, dy)
for (x1, x0, _start, _width), axis_id in axes_to_update:
lbound, hbound = self.get_axis_limits(axis_id)
i_lbound = self.transform(axis_id, lbound)
i_hbound = self.transform(axis_id, hbound)
delta = x1 - x0
vmin = self.invTransform(axis_id, i_lbound - delta)
vmax = self.invTransform(axis_id, i_hbound - delta)
self.set_axis_limits(axis_id, vmin, vmax)
self.setAutoReplot(auto)
self.replot()
# the signal MUST be emitted after replot, otherwise
# we receiver won't see the new bounds (don't know why?)
self.SIG_PLOT_AXIS_CHANGED.emit(self)
[docs] def do_zoom_view(self, dx, dy, lock_aspect_ratio=False):
"""
Change the scale of the active axes (zoom/dezoom) according to dx, dy
dx, dy are tuples composed of (initial pos, dest pos)
We try to keep initial pos fixed on the canvas as the scale changes
"""
# See guiqwt/events.py where dx and dy are defined like this:
# dx = (pos.x(), self.last.x(), self.start.x(), rct.width())
# dy = (pos.y(), self.last.y(), self.start.y(), rct.height())
# where:
# * self.last is the mouse position seen during last event
# * self.start is the first mouse position (here, this is the
# coordinate of the point which is at the center of the zoomed area)
# * rct is the plot rect contents
# * pos is the current mouse cursor position
auto = self.autoReplot()
self.setAutoReplot(False)
dx = (-1,) + dx # adding direction to tuple dx
dy = (1,) + dy # adding direction to tuple dy
if lock_aspect_ratio:
direction, x1, x0, start, width = dx
F = 1 + 3 * direction * float(x1 - x0) / width
axes_to_update = self.get_axes_to_update(dx, dy)
for (direction, x1, x0, start, width), axis_id in axes_to_update:
lbound, hbound = self.get_axis_limits(axis_id)
if not lock_aspect_ratio:
F = 1 + 3 * direction * float(x1 - x0) / width
if F * (hbound - lbound) == 0:
continue
if self.get_axis_scale(axis_id) == "lin":
orig = self.invTransform(axis_id, start)
vmin = orig - F * (orig - lbound)
vmax = orig + F * (hbound - orig)
else: # log scale
i_lbound = self.transform(axis_id, lbound)
i_hbound = self.transform(axis_id, hbound)
imin = start - F * (start - i_lbound)
imax = start + F * (i_hbound - start)
vmin = self.invTransform(axis_id, imin)
vmax = self.invTransform(axis_id, imax)
self.set_axis_limits(axis_id, vmin, vmax)
self.setAutoReplot(auto)
self.replot()
# the signal MUST be emitted after replot, otherwise
# we receiver won't see the new bounds (don't know why?)
self.SIG_PLOT_AXIS_CHANGED.emit(self)
def do_zoom_rect_view(self, start, end):
# XXX implement the case when axes are synchronised
x1, y1 = start.x(), start.y()
x2, y2 = end.x(), end.y()
xaxis, yaxis = self.get_active_axes()
active_axes = [(x1, x2, xaxis), (y1, y2, yaxis)]
for h1, h2, k in active_axes:
o1 = self.invTransform(k, h1)
o2 = self.invTransform(k, h2)
if o1 > o2:
o1, o2 = o2, o1
if o1 == o2:
continue
if self.get_axis_direction(k):
o1, o2 = o2, o1
self.setAxisScale(k, o1, o2)
self.replot()
self.SIG_PLOT_AXIS_CHANGED.emit(self)
[docs] def get_default_item(self):
"""Return default item, depending on plot's default item type
(e.g. for a curve plot, this is a curve item type).
Return nothing if there is more than one item matching
the default item type."""
items = self.get_items(item_type=self.DEFAULT_ITEM_TYPE)
if len(items) == 1:
return items[0]
# ---- BasePlot API ---------------------------------------------------------
[docs] def add_item(self, item, z=None):
"""
Add a *plot item* instance to this *plot widget*
* item: :py:data:`qwt.QwtPlotItem` object implementing
the :py:data:`guiqwt.interfaces.IBasePlotItem` interface
* z: item's z order (None -> z = max(self.get_items())+1)
"""
if isinstance(item, QwtPlotCurve):
item.setRenderHint(QwtPlotItem.RenderAntialiased, self.antialiased)
BasePlot.add_item(self, item, z)
[docs] def del_all_items(self, except_grid=True):
"""Del all items, eventually (default) except grid"""
items = [
item for item in self.items if not except_grid or item is not self.grid
]
self.del_items(items)
[docs] def set_active_item(self, item):
"""Override base set_active_item to change the grid's
axes according to the selected item"""
old_active = self.active_item
BasePlot.set_active_item(self, item)
if item is not None and old_active is not item:
self.grid.setAxes(item.xAxis(), item.yAxis())
[docs] def get_plot_parameters(self, key, itemparams):
if key == "grid":
self.grid.gridparam.update_param(self.grid)
itemparams.add("GridParam", self, self.grid.gridparam)
else:
BasePlot.get_plot_parameters(self, key, itemparams)
[docs] def set_item_parameters(self, itemparams):
# Grid style
dataset = itemparams.get("GridParam")
if dataset is not None:
dataset.update_grid(self.grid)
self.grid.gridparam = dataset
BasePlot.set_item_parameters(self, itemparams)
[docs] def do_autoscale(self, replot=True, axis_id=None):
"""Do autoscale on all axes"""
auto = self.autoReplot()
self.setAutoReplot(False)
# XXX implement the case when axes are synchronised
for axis_id in self.AXIS_IDS if axis_id is None else [axis_id]:
vmin, vmax = None, None
if not self.axisEnabled(axis_id):
continue
for item in self.get_items():
if (
isinstance(item, self.AUTOSCALE_TYPES)
and not item.is_empty()
and item.isVisible()
):
bounds = item.boundingRect()
if axis_id == item.xAxis():
xmin, xmax = bounds.left(), bounds.right()
if vmin is None or xmin < vmin:
vmin = xmin
if vmax is None or xmax > vmax:
vmax = xmax
elif axis_id == item.yAxis():
ymin, ymax = bounds.top(), bounds.bottom()
if vmin is None or ymin < vmin:
vmin = ymin
if vmax is None or ymax > vmax:
vmax = ymax
if vmin is None or vmax is None:
continue
if vmin == vmax: # same behavior as MATLAB
vmin -= 1
vmax += 1
elif self.get_axis_scale(axis_id) == "lin":
dv = vmax - vmin
vmin -= 0.002 * dv
vmax += 0.002 * dv
elif vmin > 0 and vmax > 0: # log scale
dv = np.log10(vmax) - np.log10(vmin)
vmin = 10 ** (np.log10(vmin) - 0.002 * dv)
vmax = 10 ** (np.log10(vmax) + 0.002 * dv)
self.set_axis_limits(axis_id, vmin, vmax)
self.setAutoReplot(auto)
if replot:
self.replot()
self.SIG_PLOT_AXIS_CHANGED.emit(self)
[docs] def set_axis_limits(self, axis_id, vmin, vmax, stepsize=0):
"""Set axis limits (minimum and maximum values)"""
axis_id = self.get_axis_id(axis_id)
vmin, vmax = sorted([vmin, vmax])
if self.get_axis_direction(axis_id):
BasePlot.set_axis_limits(self, axis_id, vmax, vmin, stepsize)
else:
BasePlot.set_axis_limits(self, axis_id, vmin, vmax, stepsize)
# ---- Public API -----------------------------------------------------------
[docs] def get_axis_direction(self, axis_id):
"""
Return axis direction of increasing values
* axis_id: axis id (BasePlot.Y_LEFT, BasePlot.X_BOTTOM, ...)
or string: 'bottom', 'left', 'top' or 'right'
"""
axis_id = self.get_axis_id(axis_id)
return self.axes_reverse[axis_id]
[docs] def set_axis_direction(self, axis_id, reverse=False):
"""
Set axis direction of increasing values
* axis_id: axis id (BasePlot.Y_LEFT, BasePlot.X_BOTTOM, ...)
or string: 'bottom', 'left', 'top' or 'right'
* reverse: False (default)
- x-axis values increase from left to right
- y-axis values increase from bottom to top
* reverse: True
- x-axis values increase from right to left
- y-axis values increase from top to bottom
"""
axis_id = self.get_axis_id(axis_id)
if reverse != self.axes_reverse[axis_id]:
self.replot()
self.axes_reverse[axis_id] = reverse
axis_map = self.canvasMap(axis_id)
self.setAxisScale(axis_id, axis_map.s2(), axis_map.s1())
self.updateAxes()
self.SIG_AXIS_DIRECTION_CHANGED.emit(self, axis_id)
[docs] def set_titles(self, title=None, xlabel=None, ylabel=None, xunit=None, yunit=None):
"""
Set plot and axes titles at once
* title: plot title
* xlabel: (bottom axis title, top axis title)
or bottom axis title only
* ylabel: (left axis title, right axis title)
or left axis title only
* xunit: (bottom axis unit, top axis unit)
or bottom axis unit only
* yunit: (left axis unit, right axis unit)
or left axis unit only
"""
if title is not None:
self.set_title(title)
if xlabel is not None:
if isinstance(xlabel, str):
xlabel = (xlabel, "")
for label, axis in zip(xlabel, ("bottom", "top")):
if label is not None:
self.set_axis_title(axis, label)
if ylabel is not None:
if isinstance(ylabel, str):
ylabel = (ylabel, "")
for label, axis in zip(ylabel, ("left", "right")):
if label is not None:
self.set_axis_title(axis, label)
if xunit is not None:
if isinstance(xunit, str):
xunit = (xunit, "")
for unit, axis in zip(xunit, ("bottom", "top")):
if unit is not None:
self.set_axis_unit(axis, unit)
if yunit is not None:
if isinstance(yunit, str):
yunit = (yunit, "")
for unit, axis in zip(yunit, ("left", "right")):
if unit is not None:
self.set_axis_unit(axis, unit)
[docs] def set_pointer(self, pointer_type):
"""
Set pointer.
Valid values of `pointer_type`:
* None: disable pointer
* "canvas": enable canvas pointer
* "curve": enable on-curve pointer
"""
self.canvas_pointer = False
self.curve_pointer = False
if pointer_type == "canvas":
self.canvas_pointer = True
elif pointer_type == "curve":
self.curve_pointer = True
[docs] def set_antialiasing(self, checked):
"""Toggle curve antialiasing"""
self.antialiased = checked
for curve in self.itemList():
if isinstance(curve, QwtPlotCurve):
curve.setRenderHint(QwtPlotItem.RenderAntialiased, self.antialiased)
[docs] def set_plot_limits(self, x0, x1, y0, y1, xaxis="bottom", yaxis="left"):
"""Set plot scale limits"""
self.set_axis_limits(yaxis, y0, y1)
self.set_axis_limits(xaxis, x0, x1)
self.updateAxes()
self.SIG_AXIS_DIRECTION_CHANGED.emit(self, self.get_axis_id(yaxis))
self.SIG_AXIS_DIRECTION_CHANGED.emit(self, self.get_axis_id(xaxis))
def set_plot_limits_synchronised(self, x0, x1, y0, y1):
for yaxis, xaxis in (("left", "bottom"), ("right", "top")):
self.set_plot_limits(x0, x1, y0, y1, xaxis=xaxis, yaxis=yaxis)
[docs] def get_plot_limits(self, xaxis="bottom", yaxis="left"):
"""Return plot scale limits"""
x0, x1 = self.get_axis_limits(xaxis)
y0, y1 = self.get_axis_limits(yaxis)
return x0, x1, y0, y1