#
# 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 PyQt4.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:
"""
from __future__ import with_statement
import sys
import warnings
import numpy as np
from guidata.qt.QtGui import (QMenu, QListWidget, QListWidgetItem, QVBoxLayout,
QToolBar, QMessageBox, QBrush, QColor, QPen,
QPolygonF)
from guidata.qt.QtCore import (Qt, QPoint, QPointF, QLineF, SIGNAL, QRectF,
QLine)
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
from guiqwt.signals import (SIG_ACTIVE_ITEM_CHANGED, SIG_ITEMS_CHANGED,
SIG_AXIS_DIRECTION_CHANGED, SIG_PLOT_AXIS_CHANGED)
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 xrange(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.)**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.maxint, 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 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)
class CurveItem(QwtPlotCurve):
[docs] """
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()
def boundingRect(self):
[docs] """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)
def set_selectable(self, state):
[docs] """Set item selectable state"""
self._can_select = state
def set_resizable(self, state):
[docs] """Set item resizable state
(or any action triggered when moving an handle, e.g. rotation)"""
self._can_resize = state
def set_movable(self, state):
[docs] """Set item movable state"""
self._can_move = state
def set_rotatable(self, state):
[docs] """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()
def serialize(self, writer):
[docs] """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')
def deserialize(self, reader):
[docs] """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()
def set_readonly(self, state):
[docs] """Set object readonly state"""
self._readonly = state
def is_readonly(self):
[docs] """Return object readonly state"""
return self._readonly
def set_private(self, state):
[docs] """Set object as private"""
self._private = state
def is_private(self):
[docs] """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):
[docs] """Select item"""
self.selected = True
self.setSymbol(SELECTED_SYMBOL)
self.invalidate_plot()
def unselect(self):
[docs] """Unselect item"""
self.selected = False
# Restoring initial curve parameters:
self.curveparam.update_curve(self)
self.invalidate_plot()
def get_data(self):
[docs] """Return curve data x, y (NumPy arrays)"""
return self._x, self._y
def set_data(self, x, y):
[docs] """
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)
def is_empty(self):
[docs] """Return True if item data is empty"""
return self._x is None or self._y is None or self._y.size == 0
def hit_test(self, pos):
[docs] """Calcul de la distance d'un point à une courbe
renvoie (dist, handle, inside)"""
if self.is_empty():
return sys.maxint, 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.maxint, 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
def get_closest_coordinates(self, x, y):
[docs] """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(QPoint(xc, yc))
x = self.x(i)
y = self.y(i)
return x, 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()
def move_local_shape(self, old_pos, new_pos):
[docs] """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)
def move_with_selection(self, delta_x, delta_y):
[docs] """
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 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.maxint, 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 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 xrange(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
class ErrorBarCurveItem(CurveItem):
[docs] """
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 = {}
def serialize(self, writer):
[docs] """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')
def deserialize(self, reader):
[docs] """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()
def unselect(self):
[docs] """Unselect item"""
CurveItem.unselect(self)
self.errorbarparam.update_curve(self)
def get_data(self):
[docs] """
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
def set_data(self, x, y, dx=None, dy=None):
[docs] """
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]
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(QPoint(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
def boundingRect(self):
[docs] """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)
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 = xrange(len(tx))
if self.errorOnTop:
QwtPlotCurve.draw(self, painter, xMap, yMap, canvasRect)
painter.save()
painter.setPen(self.errorPen)
cap = self.errorCap/2.
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(QLine(txmin[i], yi, txmax[i], yi))
painter.drawLines(lines)
if cap > 0:
lines = []
for i in RN:
yi = ty[i]
lines.append(QLine(txmin[i], yi-cap, txmin[i], yi+cap))
lines.append(QLine(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(QLine(xi, tymin[i], xi, tymax[i]))
painter.drawLines(lines)
if cap > 0:
# Cap
lines = []
for i in RN:
xi = tx[i]
lines.append(QLine(xi-cap, tymin[i], xi+cap, tymin[i]))
lines.append(QLine(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(QPoint(xi, tymin[i]))
rpoints.append(QPoint(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 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.connect(self, SIGNAL('currentRowChanged(int)'),
self.current_row_changed)
self.connect(self, SIGNAL('itemChanged(QListWidgetItem*)'),
self.item_changed)
self.connect(self, SIGNAL('itemSelectionChanged()'),
self.refresh_actions)
self.connect(self, SIGNAL('itemSelectionChanged()'),
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():
self.connect(plot, SIG_ITEMS_CHANGED, self.items_changed)
self.connect(plot, SIG_ACTIVE_ITEM_CHANGED, 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 range(indexes[0], indexes[-1]+1) == indexes
def get_selected_items(self):
"""Return selected QwtPlot items
Warning: this is not the same as self.plot.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():
item.setVisible(visible)
self.plot.replot()
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()
class PlotItemList(PanelWidget):
[docs] """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)
def register_panel(self, manager):
[docs] """Register panel to plot manager"""
self.manager = manager
self.listwidget.register_panel(manager)
def configure_panel(self):
class CurvePlot(BasePlot):
[docs] """
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)
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:
# (PyQwt'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.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)
# Background color
self.setCanvasBackground(Qt.white)
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:
canvas.removeEventFilter(self.filter)
# 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.setAxis(curve.xAxis(), curve.yAxis())
self.curve_marker.setAxis(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.move_curve_marker(self.curve_marker, xc, yc)
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.move_canvas_marker(self.cross_marker, xc, yc)
else:
vis_cross = self.cross_marker.isVisible()
vis_curve = self.curve_marker.isVisible()
self.cross_marker.setVisible(False)
self.curve_marker.setVisible(False)
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)]
def do_pan_view(self, dx, dy):
[docs] """
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.emit(SIG_PLOT_AXIS_CHANGED, self)
def do_zoom_view(self, dx, dy, lock_aspect_ratio=False):
[docs] """
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.emit(SIG_PLOT_AXIS_CHANGED, 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()
def get_default_item(self):
[docs] """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 ---------------------------------------------------------
def add_item(self, item, z=None):
[docs] """
Add a *plot item* instance to this *plot widget*
* item: QwtPlotItem (PyQt4.Qwt5) object implementing
the IBasePlotItem interface (guiqwt.interfaces)
* 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)
def del_all_items(self, except_grid=True):
[docs] """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)
def set_active_item(self, item):
[docs] """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.setAxis(item.xAxis(), item.yAxis())
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)
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)
def do_autoscale(self, replot=True):
[docs] """Do autoscale on all axes"""
# XXX implement the case when axes are synchronised
for axis_id in self.AXIS_IDS:
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 -= .002*dv
vmax += .002*dv
elif vmin > 0 and vmax > 0: # log scale
dv = np.log10(vmax)-np.log10(vmin)
vmin = 10**(np.log10(vmin)-.002*dv)
vmax = 10**(np.log10(vmax)+.002*dv)
self.set_axis_limits(axis_id, vmin, vmax)
if replot:
self.replot()
def set_axis_limits(self, axis_id, vmin, vmax, stepsize=0):
[docs] """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 -----------------------------------------------------------
def get_axis_direction(self, axis_id):
[docs] """
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]
def set_axis_direction(self, axis_id, reverse=False):
[docs] """
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.emit(SIG_AXIS_DIRECTION_CHANGED, self, axis_id)
def set_titles(self, title=None, xlabel=None, ylabel=None,
[docs] 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, basestring):
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, basestring):
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, basestring):
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, basestring):
yunit = (yunit, "")
for unit, axis in zip(yunit, ("left", "right")):
if unit is not None:
self.set_axis_unit(axis, unit)
def set_pointer(self, pointer_type):
[docs] """
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
def set_antialiasing(self, checked):
[docs] """Toggle curve antialiasing"""
self.antialiased = checked
for curve in self.itemList():
if isinstance(curve, QwtPlotCurve):
curve.setRenderHint(QwtPlotItem.RenderAntialiased,
self.antialiased)
def set_plot_limits(self, x0, x1, y0, y1, xaxis="bottom", yaxis="left"):
[docs] """Set plot scale limits"""
self.set_axis_limits(yaxis, y0, y1)
self.set_axis_limits(xaxis, x0, x1)
self.updateAxes()
self.emit(SIG_AXIS_DIRECTION_CHANGED, self, self.get_axis_id(yaxis))
self.emit(SIG_AXIS_DIRECTION_CHANGED, 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)
def get_plot_limits(self, xaxis="bottom", yaxis="left"):
[docs] """Return plot scale limits"""
x0, x1 = self.get_axis_limits(xaxis)
y0, y1 = self.get_axis_limits(yaxis)
return x0, x1, y0, y1