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 PyQt5.QtGui import QApplication
>>> app = QApplication([])

    * now that a `QApplication` object exists, we may create the plotting
      widget:

>>> from guiqwt.curve import CurvePlot
>>> plot = CurvePlot(title="Example", xlabel="X", ylabel="Y")

Create a curve item:
    * from the associated plot item class (e.g. `ErrorBarCurveItem` to
      create a curve with error bars): the item properties are then assigned
      by creating the appropriate style parameters object
      (e.g. :py:class:`guiqwt.styles.ErrorBarParam`)

>>> from guiqwt.curve import CurveItem
>>> from guiqwt.styles import CurveParam
>>> param = CurveParam()
>>> param.label = 'My curve'
>>> curve = CurveItem(param)
>>> curve.set_data(x, y)

    * or using the `plot item builder` (see :py:func:`guiqwt.builder.make`):

>>> from guiqwt.builder import make
>>> curve = make.curve(x, y, title='My curve')

Attach the curve to the plotting widget:

>>> plot.add_item(curve)

Display the plotting widget:

>>> plot.show()
>>> app.exec_()

Reference
~~~~~~~~~

.. autoclass:: CurvePlot
   :members:
   :inherited-members:
.. autoclass:: CurveItem
   :members:
   :inherited-members:
.. autoclass:: ErrorBarCurveItem
   :members:
   :inherited-members:
.. autoclass:: PlotItemList
   :members:
"""

import warnings
import numpy as np
import sys

from qtpy.QtWidgets import (
    QMenu,
    QListWidget,
    QListWidgetItem,
    QVBoxLayout,
    QToolBar,
    QMessageBox,
)
from qtpy.QtGui import QBrush, QColor, QPen, QPolygonF
from qtpy.QtCore import Qt, QPointF, QLineF, QRectF, Signal

from guidata.utils import assert_interfaces_valid, update_dataset
from guidata.configtools import get_icon, get_image_layout
from guidata.qthelpers import create_action, add_actions

# Local imports
from guiqwt.transitional import QwtPlotCurve, QwtPlotGrid, QwtPlotItem, QwtScaleMap
from guiqwt.config import CONF, _
from guiqwt.interfaces import (
    IBasePlotItem,
    IDecoratorItemType,
    ISerializableType,
    ICurveItemType,
    ITrackableItemType,
    IPanel,
)
from guiqwt.panels import PanelWidget, ID_ITEMLIST
from guiqwt.baseplot import BasePlot, canvas_to_axes
from guiqwt.styles import GridParam, CurveParam, ErrorBarParam, SymbolParam
from guiqwt.shapes import Marker


def _simplify_poly(pts, off, scale, bounds):
    ax, bx, ay, by = scale
    xm, ym, xM, yM = bounds
    a = np.array([[ax, ay]])
    b = np.array([[bx, by]])
    _pts = a * pts + b
    poly = []
    NP = off.shape[0]
    for i in range(off.shape[0]):
        i0 = off[i, 1]
        if i + 1 < NP:
            i1 = off[i + 1, 1]
        else:
            i1 = pts.shape[0]
        poly.append((_pts[i0:i1], i))
    return poly


try:
    from gshhs import simplify_poly
except ImportError:
    simplify_poly = _simplify_poly


def seg_dist(P, P0, P1):
    """
    Return distance between point P and segment (P0, P1)
    If P orthogonal projection on (P0, P1) is outside segment bounds, return
    either distance to P0 or to P1 (the closest one)
    P, P0, P1: QPointF instances
    """
    u = QLineF(P0, P).length()
    if P0 == P1:
        return u
    else:
        angle = QLineF(P0, P).angleTo(QLineF(P0, P1)) * np.pi / 180
        projection = u * np.cos(angle)
        if projection > QLineF(P0, P1).length():
            return QLineF(P1, P).length()
        elif projection < 0:
            return QLineF(P0, P).length()
        else:
            return abs(u * np.sin(angle))


def test_seg_dist():
    print(seg_dist(QPointF(200, 100), QPointF(150, 196), QPointF(250, 180)))
    print(seg_dist(QPointF(200, 100), QPointF(190, 196), QPointF(210, 180)))
    print(seg_dist(QPointF(201, 105), QPointF(201, 196), QPointF(201, 180)))


def norm2(v):
    return (v**2).sum(axis=1)


def seg_dist_v(P, X0, Y0, X1, Y1):
    """Version vectorielle de seg_dist"""
    V = np.zeros((X0.shape[0], 2), float)
    PP = np.zeros((X0.shape[0], 2), float)
    PP[:, 0] = X0
    PP[:, 1] = Y0
    V[:, 0] = X1 - X0
    V[:, 1] = Y1 - Y0
    dP = np.array(P).reshape(1, 2) - PP
    nV = np.sqrt(norm2(V)).clip(1e-12)  # clip: avoid division by zero
    w2 = V / nV[:, np.newaxis]
    w = np.array([-w2[:, 1], w2[:, 0]]).T
    distances = np.fabs((dP * w).sum(axis=1))
    ix = distances.argmin()
    return ix, distances[ix]


def test_seg_dist_v():
    """Test de seg_dist_v"""
    a = (np.arange(10.0) ** 2).reshape(5, 2)
    ix, dist = seg_dist_v((2.1, 3.3), a[:-1, 0], a[:-1, 1], a[1:, 0], a[1:, 1])
    print(ix, dist)
    assert ix == 0


if __name__ == "__main__":
    test_seg_dist_v()
    test_seg_dist()


SELECTED_SYMBOL_PARAM = SymbolParam()
SELECTED_SYMBOL_PARAM.read_config(CONF, "plot", "selected_curve_symbol")
SELECTED_SYMBOL = SELECTED_SYMBOL_PARAM.build_symbol()


class GridItem(QwtPlotGrid):
    """
    Construct a grid `plot item` with the parameters *gridparam*
    (see :py:class:`guiqwt.styles.GridParam`)
    """

    __implements__ = (IBasePlotItem,)

    _readonly = True
    _private = False

    def __init__(self, gridparam=None):
        super(GridItem, self).__init__()
        if gridparam is None:
            self.gridparam = GridParam(title=_("Grid"), icon="grid.png")
        else:
            self.gridparam = gridparam
        self.selected = False
        self.immutable = True  # set to false to allow moving points around
        self.update_params()  # won't work completely because it's not yet
        # attached to plot (actually, only canvas background won't be updated)

    def types(self):
        return (IDecoratorItemType,)

    def attach(self, plot):
        """Reimplemented to update plot canvas background"""
        QwtPlotGrid.attach(self, plot)
        self.update_params()

    def set_readonly(self, state):
        """Set object read-only state"""
        self._readonly = state

    def is_readonly(self):
        """Return object read-only state"""
        return self._readonly

    def set_private(self, state):
        """Set object as private"""
        self._private = state

    def is_private(self):
        """Return True if object is private"""
        return self._private

    def set_selectable(self, state):
        """Set item selectable state"""
        self._can_select = state

    def set_resizable(self, state):
        """Set item resizable state
        (or any action triggered when moving an handle, e.g. rotation)"""
        self._can_resize = state

    def set_movable(self, state):
        """Set item movable state"""
        self._can_move = state

    def set_rotatable(self, state):
        """Set item rotatable state"""
        self._can_rotate = state

    def can_select(self):
        return False

    def can_resize(self):
        return False

    def can_rotate(self):
        return False

    def can_move(self):
        return False

    def select(self):
        """Select item"""
        pass

    def unselect(self):
        """Unselect item"""
        pass

    def hit_test(self, pos):
        return sys.maxsize, 0, False, None

    def move_local_point_to(self, handle, pos, ctrl=None):
        pass

    def move_local_shape(self, old_pos, new_pos):
        pass

    def move_with_selection(self, delta_x, delta_y):
        pass

    def update_params(self):
        self.gridparam.update_grid(self)

    def update_item_parameters(self):
        self.gridparam.update_param(self)

    def get_item_parameters(self, itemparams):
        itemparams.add("GridParam", self, self.gridparam)

    def set_item_parameters(self, itemparams):
        self.gridparam = itemparams.get("GridParam")
        self.gridparam.update_grid(self)


assert_interfaces_valid(GridItem)


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