Source code for guiqwt.curve

# -*- 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 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, print_function

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, 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
from guidata.py3compat import is_text_string, maxsize

# 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.)**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 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 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 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 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] 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)
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. 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)
[docs] def configure_panel(self): """Configure panel""" pass
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("PyQt_PyObject") 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.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.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.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)]
[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())
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)
[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 -= .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) 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 is_text_string(xlabel): 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 is_text_string(ylabel): 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 is_text_string(xunit): 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 is_text_string(yunit): 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