# 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.misc import assert_interfaces_valid
    from guidata.dataset import 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)

    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()

        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)

        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 True

        def can_resize(self):
            return False

        def can_rotate(self):
            return False

        def can_move(self):
            return False

        def __reduce__(self):
            state = (self.curveparam, self._x, self._y, self.z())
            res = (CurveItem, (), state)
            return res

        def __setstate__(self, state):
            param, x, y, z = state
            self.curveparam = param
            self.set_data(x, y)
            self.setZ(z)
            self.update_params()

        def serialize(self, writer):
            """Serialize object to HDF5 writer"""
            writer.write(self._x, group_name="Xdata")
            writer.write(self._y, group_name="Ydata")
            writer.write(self.z(), group_name="z")
            self.curveparam.update_param(self)
            writer.write(self.curveparam, group_name="curveparam")

        def deserialize(self, reader):
            """Deserialize object from HDF5 reader"""
            self.curveparam = CurveParam(_("Curve"), icon="curve.png")
            reader.read("curveparam", instance=self.curveparam)
            x = reader.read(group_name="Xdata", func=reader.read_array)
            y = reader.read(group_name="Ydata", func=reader.read_array)
            self.set_data(x, y)
            self.setZ(reader.read("z"))
            self.update_params()

        def set_readonly(self, state):
            """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
            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()

        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._x, self._y

        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)

        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

        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

        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()

        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)

        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

    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 = {}

        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")

        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()

        def unselect(self):
            """Unselect item"""
            CurveItem.unselect(self)
            self.errorbarparam.update_curve(self)

        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

        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

        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.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()

    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)

        def register_panel(self, manager):
            """Register panel to plot manager"""
            self.manager = manager
            self.listwidget.register_panel(manager)

        def configure_panel(self):
            """Configure panel"""
            pass

    assert_interfaces_valid(PlotItemList)

    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)]

        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)

        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)

        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 ---------------------------------------------------------

        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)

        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)

        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)

        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)

        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 -----------------------------------------------------------

        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]

        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)

        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)

        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

        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)

        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)

        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