summaryrefslogtreecommitdiff
path: root/silx/gui/plot/items
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot/items')
-rw-r--r--silx/gui/plot/items/__init__.py52
-rw-r--r--silx/gui/plot/items/_arc_roi.py878
-rw-r--r--silx/gui/plot/items/_pick.py72
-rw-r--r--silx/gui/plot/items/_roi_base.py835
-rw-r--r--silx/gui/plot/items/axis.py569
-rw-r--r--silx/gui/plot/items/complex.py386
-rw-r--r--silx/gui/plot/items/core.py1734
-rw-r--r--silx/gui/plot/items/curve.py326
-rw-r--r--silx/gui/plot/items/histogram.py389
-rw-r--r--silx/gui/plot/items/image.py617
-rwxr-xr-xsilx/gui/plot/items/marker.py281
-rw-r--r--silx/gui/plot/items/roi.py1519
-rw-r--r--silx/gui/plot/items/scatter.py973
-rw-r--r--silx/gui/plot/items/shape.py288
14 files changed, 0 insertions, 8919 deletions
diff --git a/silx/gui/plot/items/__init__.py b/silx/gui/plot/items/__init__.py
deleted file mode 100644
index 0484025..0000000
--- a/silx/gui/plot/items/__init__.py
+++ /dev/null
@@ -1,52 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""This package provides classes that describes :class:`.PlotWidget` content.
-
-Instances of those classes are returned by :class:`.PlotWidget` methods that give
-access to its content such as :meth:`.PlotWidget.getCurve`, :meth:`.PlotWidget.getImage`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "22/06/2017"
-
-from .core import (Item, DataItem, # noqa
- LabelsMixIn, DraggableMixIn, ColormapMixIn, # noqa
- SymbolMixIn, ColorMixIn, YAxisMixIn, FillMixIn, # noqa
- AlphaMixIn, LineMixIn, ScatterVisualizationMixIn, # noqa
- ComplexMixIn, ItemChangedType, PointsBase) # noqa
-from .complex import ImageComplexData # noqa
-from .curve import Curve, CurveStyle # noqa
-from .histogram import Histogram # noqa
-from .image import ImageBase, ImageData, ImageRgba, ImageStack, MaskImageData # noqa
-from .shape import Shape, BoundingRect, XAxisExtent, YAxisExtent # noqa
-from .scatter import Scatter # noqa
-from .marker import MarkerBase, Marker, XMarker, YMarker # noqa
-from .axis import Axis, XAxis, YAxis, YRightAxis
-
-DATA_ITEMS = (ImageComplexData, Curve, Histogram, ImageBase, Scatter,
- BoundingRect, XAxisExtent, YAxisExtent)
-"""Classes of items representing data and to consider to compute data bounds.
-"""
diff --git a/silx/gui/plot/items/_arc_roi.py b/silx/gui/plot/items/_arc_roi.py
deleted file mode 100644
index 23416ec..0000000
--- a/silx/gui/plot/items/_arc_roi.py
+++ /dev/null
@@ -1,878 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018-2021 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""This module provides Arc ROI item for the :class:`~silx.gui.plot.PlotWidget`.
-"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "28/06/2018"
-
-import logging
-import numpy
-
-from ... import utils
-from .. import items
-from ...colors import rgba
-from ....utils.proxy import docstring
-from ._roi_base import HandleBasedROI
-from ._roi_base import InteractionModeMixIn
-from ._roi_base import RoiInteractionMode
-
-
-logger = logging.getLogger(__name__)
-
-
-class _ArcGeometry:
- """
- Non-mutable object to store the geometry of the arc ROI.
-
- The aim is is to switch between consistent state without dealing with
- intermediate values.
- """
- def __init__(self, center, startPoint, endPoint, radius,
- weight, startAngle, endAngle, closed=False):
- """Constructor for a consistent arc geometry.
-
- There is also specific class method to create different kind of arc
- geometry.
- """
- self.center = center
- self.startPoint = startPoint
- self.endPoint = endPoint
- self.radius = radius
- self.weight = weight
- self.startAngle = startAngle
- self.endAngle = endAngle
- self._closed = closed
-
- @classmethod
- def createEmpty(cls):
- """Create an arc geometry from an empty shape
- """
- zero = numpy.array([0, 0])
- return cls(zero, zero.copy(), zero.copy(), 0, 0, 0, 0)
-
- @classmethod
- def createRect(cls, startPoint, endPoint, weight):
- """Create an arc geometry from a definition of a rectangle
- """
- return cls(None, startPoint, endPoint, None, weight, None, None, False)
-
- @classmethod
- def createCircle(cls, center, startPoint, endPoint, radius,
- weight, startAngle, endAngle):
- """Create an arc geometry from a definition of a circle
- """
- return cls(center, startPoint, endPoint, radius,
- weight, startAngle, endAngle, True)
-
- def withWeight(self, weight):
- """Return a new geometry based on this object, with a specific weight
- """
- return _ArcGeometry(self.center, self.startPoint, self.endPoint,
- self.radius, weight,
- self.startAngle, self.endAngle, self._closed)
-
- def withRadius(self, radius):
- """Return a new geometry based on this object, with a specific radius.
-
- The weight and the center is conserved.
- """
- startPoint = self.center + (self.startPoint - self.center) / self.radius * radius
- endPoint = self.center + (self.endPoint - self.center) / self.radius * radius
- return _ArcGeometry(self.center, startPoint, endPoint,
- radius, self.weight,
- self.startAngle, self.endAngle, self._closed)
-
- def withStartAngle(self, startAngle):
- """Return a new geometry based on this object, with a specific start angle
- """
- vector = numpy.array([numpy.cos(startAngle), numpy.sin(startAngle)])
- startPoint = self.center + vector * self.radius
-
- # Never add more than 180 to maintain coherency
- deltaAngle = startAngle - self.startAngle
- if deltaAngle > numpy.pi:
- deltaAngle -= numpy.pi * 2
- elif deltaAngle < -numpy.pi:
- deltaAngle += numpy.pi * 2
-
- startAngle = self.startAngle + deltaAngle
- return _ArcGeometry(
- self.center,
- startPoint,
- self.endPoint,
- self.radius,
- self.weight,
- startAngle,
- self.endAngle,
- self._closed,
- )
-
- def withEndAngle(self, endAngle):
- """Return a new geometry based on this object, with a specific end angle
- """
- vector = numpy.array([numpy.cos(endAngle), numpy.sin(endAngle)])
- endPoint = self.center + vector * self.radius
-
- # Never add more than 180 to maintain coherency
- deltaAngle = endAngle - self.endAngle
- if deltaAngle > numpy.pi:
- deltaAngle -= numpy.pi * 2
- elif deltaAngle < -numpy.pi:
- deltaAngle += numpy.pi * 2
-
- endAngle = self.endAngle + deltaAngle
- return _ArcGeometry(
- self.center,
- self.startPoint,
- endPoint,
- self.radius,
- self.weight,
- self.startAngle,
- endAngle,
- self._closed,
- )
-
- def translated(self, dx, dy):
- """Return the translated geometry by dx, dy"""
- delta = numpy.array([dx, dy])
- center = None if self.center is None else self.center + delta
- startPoint = None if self.startPoint is None else self.startPoint + delta
- endPoint = None if self.endPoint is None else self.endPoint + delta
- return _ArcGeometry(center, startPoint, endPoint,
- self.radius, self.weight,
- self.startAngle, self.endAngle, self._closed)
-
- def getKind(self):
- """Returns the kind of shape defined"""
- if self.center is None:
- return "rect"
- elif numpy.isnan(self.startAngle):
- return "point"
- elif self.isClosed():
- if self.weight <= 0 or self.weight * 0.5 >= self.radius:
- return "circle"
- else:
- return "donut"
- else:
- if self.weight * 0.5 < self.radius:
- return "arc"
- else:
- return "camembert"
-
- def isClosed(self):
- """Returns True if the geometry is a circle like"""
- if self._closed is not None:
- return self._closed
- delta = numpy.abs(self.endAngle - self.startAngle)
- self._closed = numpy.isclose(delta, numpy.pi * 2)
- return self._closed
-
- def __str__(self):
- return str((self.center,
- self.startPoint,
- self.endPoint,
- self.radius,
- self.weight,
- self.startAngle,
- self.endAngle,
- self._closed))
-
-
-class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
- """A ROI identifying an arc of a circle with a width.
-
- This ROI provides
- - 3 handle to control the curvature
- - 1 handle to control the weight
- - 1 anchor to translate the shape.
- """
-
- ICON = 'add-shape-arc'
- NAME = 'arc ROI'
- SHORT_NAME = "arc"
- """Metadata for this kind of ROI"""
-
- _plotShape = "line"
- """Plot shape which is used for the first interaction"""
-
- ThreePointMode = RoiInteractionMode("3 points", "Provides 3 points to define the main radius circle")
- PolarMode = RoiInteractionMode("Polar", "Provides anchors to edit the ROI in polar coords")
- # FIXME: MoveMode was designed cause there is too much anchors
- # FIXME: It would be good replace it by a dnd on the shape
- MoveMode = RoiInteractionMode("Translation", "Provides anchors to only move the ROI")
-
- def __init__(self, parent=None):
- HandleBasedROI.__init__(self, parent=parent)
- items.LineMixIn.__init__(self)
- InteractionModeMixIn.__init__(self)
-
- self._geometry = _ArcGeometry.createEmpty()
- self._handleLabel = self.addLabelHandle()
-
- self._handleStart = self.addHandle()
- self._handleMid = self.addHandle()
- self._handleEnd = self.addHandle()
- self._handleWeight = self.addHandle()
- self._handleWeight._setConstraint(self._arcCurvatureMarkerConstraint)
- self._handleMove = self.addTranslateHandle()
-
- shape = items.Shape("polygon")
- shape.setPoints([[0, 0], [0, 0]])
- shape.setColor(rgba(self.getColor()))
- shape.setFill(False)
- shape.setOverlay(True)
- shape.setLineStyle(self.getLineStyle())
- shape.setLineWidth(self.getLineWidth())
- self.__shape = shape
- self.addItem(shape)
-
- self._initInteractionMode(self.ThreePointMode)
- self._interactiveModeUpdated(self.ThreePointMode)
-
- def availableInteractionModes(self):
- """Returns the list of available interaction modes
-
- :rtype: List[RoiInteractionMode]
- """
- return [self.ThreePointMode, self.PolarMode, self.MoveMode]
-
- def _interactiveModeUpdated(self, modeId):
- """Set the interaction mode.
-
- :param RoiInteractionMode modeId:
- """
- if modeId is self.ThreePointMode:
- self._handleStart.setSymbol("s")
- self._handleMid.setSymbol("s")
- self._handleEnd.setSymbol("s")
- self._handleWeight.setSymbol("d")
- self._handleMove.setSymbol("+")
- elif modeId is self.PolarMode:
- self._handleStart.setSymbol("o")
- self._handleMid.setSymbol("o")
- self._handleEnd.setSymbol("o")
- self._handleWeight.setSymbol("d")
- self._handleMove.setSymbol("+")
- elif modeId is self.MoveMode:
- self._handleStart.setSymbol("")
- self._handleMid.setSymbol("+")
- self._handleEnd.setSymbol("")
- self._handleWeight.setSymbol("")
- self._handleMove.setSymbol("+")
- else:
- assert False
- if self._geometry.isClosed():
- if modeId != self.MoveMode:
- self._handleStart.setSymbol("x")
- self._handleEnd.setSymbol("x")
- self._updateHandles()
-
- def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.VISIBLE:
- self._updateItemProperty(event, self, self.__shape)
- super(ArcROI, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- super(ArcROI, self)._updatedStyle(event, style)
- self.__shape.setColor(style.getColor())
- self.__shape.setLineStyle(style.getLineStyle())
- self.__shape.setLineWidth(style.getLineWidth())
-
- def setFirstShapePoints(self, points):
- """"Initialize the ROI using the points from the first interaction.
-
- This interaction is constrained by the plot API and only supports few
- shapes.
- """
- # The first shape is a line
- point0 = points[0]
- point1 = points[1]
-
- # Compute a non collinear point for the curvature
- center = (point1 + point0) * 0.5
- normal = point1 - center
- normal = numpy.array((normal[1], -normal[0]))
- defaultCurvature = numpy.pi / 5.0
- weightCoef = 0.20
- mid = center - normal * defaultCurvature
- distance = numpy.linalg.norm(point0 - point1)
- weight = distance * weightCoef
-
- geometry = self._createGeometryFromControlPoints(point0, mid, point1, weight)
- self._geometry = geometry
- self._updateHandles()
-
- def _updateText(self, text):
- self._handleLabel.setText(text)
-
- def _updateMidHandle(self):
- """Keep the same geometry, but update the location of the control
- points.
-
- So calling this function do not trigger sigRegionChanged.
- """
- geometry = self._geometry
-
- if geometry.isClosed():
- start = numpy.array(self._handleStart.getPosition())
- midPos = geometry.center + geometry.center - start
- else:
- if geometry.center is None:
- midPos = geometry.startPoint * 0.5 + geometry.endPoint * 0.5
- else:
- midAngle = geometry.startAngle * 0.5 + geometry.endAngle * 0.5
- vector = numpy.array([numpy.cos(midAngle), numpy.sin(midAngle)])
- midPos = geometry.center + geometry.radius * vector
-
- with utils.blockSignals(self._handleMid):
- self._handleMid.setPosition(*midPos)
-
- def _updateWeightHandle(self):
- geometry = self._geometry
- if geometry.center is None:
- # rectangle
- center = (geometry.startPoint + geometry.endPoint) * 0.5
- normal = geometry.endPoint - geometry.startPoint
- normal = numpy.array((normal[1], -normal[0]))
- distance = numpy.linalg.norm(normal)
- if distance != 0:
- normal = normal / distance
- weightPos = center + normal * geometry.weight * 0.5
- else:
- if geometry.isClosed():
- midAngle = geometry.startAngle + numpy.pi * 0.5
- elif geometry.center is not None:
- midAngle = (geometry.startAngle + geometry.endAngle) * 0.5
- vector = numpy.array([numpy.cos(midAngle), numpy.sin(midAngle)])
- weightPos = geometry.center + (geometry.radius + geometry.weight * 0.5) * vector
-
- with utils.blockSignals(self._handleWeight):
- self._handleWeight.setPosition(*weightPos)
-
- def _getWeightFromHandle(self, weightPos):
- geometry = self._geometry
- if geometry.center is None:
- # rectangle
- center = (geometry.startPoint + geometry.endPoint) * 0.5
- return numpy.linalg.norm(center - weightPos) * 2
- else:
- distance = numpy.linalg.norm(geometry.center - weightPos)
- return abs(distance - geometry.radius) * 2
-
- def _updateHandles(self):
- geometry = self._geometry
- with utils.blockSignals(self._handleStart):
- self._handleStart.setPosition(*geometry.startPoint)
- with utils.blockSignals(self._handleEnd):
- self._handleEnd.setPosition(*geometry.endPoint)
-
- self._updateMidHandle()
- self._updateWeightHandle()
- self._updateShape()
-
- def _updateCurvature(self, start, mid, end, updateCurveHandles, checkClosed=False, updateStart=False):
- """Update the curvature using 3 control points in the curve
-
- :param bool updateCurveHandles: If False curve handles are already at
- the right location
- """
- if checkClosed:
- closed = self._isCloseInPixel(start, end)
- else:
- closed = self._geometry.isClosed()
- if closed:
- if updateStart:
- start = end
- else:
- end = start
-
- if updateCurveHandles:
- with utils.blockSignals(self._handleStart):
- self._handleStart.setPosition(*start)
- with utils.blockSignals(self._handleMid):
- self._handleMid.setPosition(*mid)
- with utils.blockSignals(self._handleEnd):
- self._handleEnd.setPosition(*end)
-
- weight = self._geometry.weight
- geometry = self._createGeometryFromControlPoints(start, mid, end, weight, closed=closed)
- self._geometry = geometry
-
- self._updateWeightHandle()
- self._updateShape()
-
- def _updateCloseInAngle(self, geometry, updateStart):
- azim = numpy.abs(geometry.endAngle - geometry.startAngle)
- if numpy.pi < azim < 3 * numpy.pi:
- closed = self._isCloseInPixel(geometry.startPoint, geometry.endPoint)
- geometry._closed = closed
- if closed:
- sign = 1 if geometry.startAngle < geometry.endAngle else -1
- if updateStart:
- geometry.startPoint = geometry.endPoint
- geometry.startAngle = geometry.endAngle - sign * 2*numpy.pi
- else:
- geometry.endPoint = geometry.startPoint
- geometry.endAngle = geometry.startAngle + sign * 2*numpy.pi
-
- def handleDragUpdated(self, handle, origin, previous, current):
- modeId = self.getInteractionMode()
- if handle is self._handleStart:
- if modeId is self.ThreePointMode:
- mid = numpy.array(self._handleMid.getPosition())
- end = numpy.array(self._handleEnd.getPosition())
- self._updateCurvature(
- current, mid, end, checkClosed=True, updateStart=True,
- updateCurveHandles=False
- )
- elif modeId is self.PolarMode:
- v = current - self._geometry.center
- startAngle = numpy.angle(complex(v[0], v[1]))
- geometry = self._geometry.withStartAngle(startAngle)
- self._updateCloseInAngle(geometry, updateStart=True)
- self._geometry = geometry
- self._updateHandles()
- elif handle is self._handleMid:
- if modeId is self.ThreePointMode:
- if self._geometry.isClosed():
- radius = numpy.linalg.norm(self._geometry.center - current)
- self._geometry = self._geometry.withRadius(radius)
- self._updateHandles()
- else:
- start = numpy.array(self._handleStart.getPosition())
- end = numpy.array(self._handleEnd.getPosition())
- self._updateCurvature(start, current, end, updateCurveHandles=False)
- elif modeId is self.PolarMode:
- radius = numpy.linalg.norm(self._geometry.center - current)
- self._geometry = self._geometry.withRadius(radius)
- self._updateHandles()
- elif modeId is self.MoveMode:
- delta = current - previous
- self.translate(*delta)
- elif handle is self._handleEnd:
- if modeId is self.ThreePointMode:
- start = numpy.array(self._handleStart.getPosition())
- mid = numpy.array(self._handleMid.getPosition())
- self._updateCurvature(
- start, mid, current, checkClosed=True, updateStart=False,
- updateCurveHandles=False
- )
- elif modeId is self.PolarMode:
- v = current - self._geometry.center
- endAngle = numpy.angle(complex(v[0], v[1]))
- geometry = self._geometry.withEndAngle(endAngle)
- self._updateCloseInAngle(geometry, updateStart=False)
- self._geometry = geometry
- self._updateHandles()
- elif handle is self._handleWeight:
- weight = self._getWeightFromHandle(current)
- self._geometry = self._geometry.withWeight(weight)
- self._updateShape()
- elif handle is self._handleMove:
- delta = current - previous
- self.translate(*delta)
-
- def _isCloseInPixel(self, point1, point2):
- manager = self.parent()
- if manager is None:
- return False
- plot = manager.parent()
- if plot is None:
- return False
- point1 = plot.dataToPixel(*point1)
- if point1 is None:
- return False
- point2 = plot.dataToPixel(*point2)
- if point2 is None:
- return False
- return abs(point1[0] - point2[0]) + abs(point1[1] - point2[1]) < 15
-
- def _normalizeGeometry(self):
- """Keep the same phisical geometry, but with normalized parameters.
- """
- geometry = self._geometry
- if geometry.weight * 0.5 >= geometry.radius:
- radius = (geometry.weight * 0.5 + geometry.radius) * 0.5
- geometry = geometry.withRadius(radius)
- geometry = geometry.withWeight(radius * 2)
- self._geometry = geometry
- return True
- return False
-
- def handleDragFinished(self, handle, origin, current):
- modeId = self.getInteractionMode()
- if handle in [self._handleStart, self._handleMid, self._handleEnd]:
- if modeId is self.ThreePointMode:
- self._normalizeGeometry()
- self._updateHandles()
-
- if self._geometry.isClosed():
- if modeId is self.MoveMode:
- self._handleStart.setSymbol("")
- self._handleEnd.setSymbol("")
- else:
- self._handleStart.setSymbol("x")
- self._handleEnd.setSymbol("x")
- else:
- if modeId is self.ThreePointMode:
- self._handleStart.setSymbol("s")
- self._handleEnd.setSymbol("s")
- elif modeId is self.PolarMode:
- self._handleStart.setSymbol("o")
- self._handleEnd.setSymbol("o")
- if modeId is self.MoveMode:
- self._handleStart.setSymbol("")
- self._handleEnd.setSymbol("")
-
- def _createGeometryFromControlPoints(self, start, mid, end, weight, closed=None):
- """Returns the geometry of the object"""
- if closed or (closed is None and numpy.allclose(start, end)):
- # Special arc: It's a closed circle
- center = (start + mid) * 0.5
- radius = numpy.linalg.norm(start - center)
- v = start - center
- startAngle = numpy.angle(complex(v[0], v[1]))
- endAngle = startAngle + numpy.pi * 2.0
- return _ArcGeometry.createCircle(
- center, start, end, radius, weight, startAngle, endAngle
- )
-
- elif numpy.linalg.norm(numpy.cross(mid - start, end - start)) < 1e-5:
- # Degenerated arc, it's a rectangle
- return _ArcGeometry.createRect(start, end, weight)
- else:
- center, radius = self._circleEquation(start, mid, end)
- v = start - center
- startAngle = numpy.angle(complex(v[0], v[1]))
- v = mid - center
- midAngle = numpy.angle(complex(v[0], v[1]))
- v = end - center
- endAngle = numpy.angle(complex(v[0], v[1]))
-
- # Is it clockwise or anticlockwise
- relativeMid = (endAngle - midAngle + 2 * numpy.pi) % (2 * numpy.pi)
- relativeEnd = (endAngle - startAngle + 2 * numpy.pi) % (2 * numpy.pi)
- if relativeMid < relativeEnd:
- if endAngle < startAngle:
- endAngle += 2 * numpy.pi
- else:
- if endAngle > startAngle:
- endAngle -= 2 * numpy.pi
-
- return _ArcGeometry(center, start, end,
- radius, weight, startAngle, endAngle)
-
- def _createShapeFromGeometry(self, geometry):
- kind = geometry.getKind()
- if kind == "rect":
- # It is not an arc
- # but we can display it as an intermediate shape
- normal = geometry.endPoint - geometry.startPoint
- normal = numpy.array((normal[1], -normal[0]))
- distance = numpy.linalg.norm(normal)
- if distance != 0:
- normal /= distance
- points = numpy.array([
- geometry.startPoint + normal * geometry.weight * 0.5,
- geometry.endPoint + normal * geometry.weight * 0.5,
- geometry.endPoint - normal * geometry.weight * 0.5,
- geometry.startPoint - normal * geometry.weight * 0.5])
- elif kind == "point":
- # It is not an arc
- # but we can display it as an intermediate shape
- # NOTE: At least 2 points are expected
- points = numpy.array([geometry.startPoint, geometry.startPoint])
- elif kind == "circle":
- outerRadius = geometry.radius + geometry.weight * 0.5
- angles = numpy.linspace(0, 2 * numpy.pi, num=50)
- # It's a circle
- points = []
- numpy.append(angles, angles[-1])
- for angle in angles:
- direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
- points.append(geometry.center + direction * outerRadius)
- points = numpy.array(points)
- elif kind == "donut":
- innerRadius = geometry.radius - geometry.weight * 0.5
- outerRadius = geometry.radius + geometry.weight * 0.5
- angles = numpy.linspace(0, 2 * numpy.pi, num=50)
- # It's a donut
- points = []
- # NOTE: NaN value allow to create 2 separated circle shapes
- # using a single plot item. It's a kind of cheat
- points.append(numpy.array([float("nan"), float("nan")]))
- for angle in angles:
- direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
- points.insert(0, geometry.center + direction * innerRadius)
- points.append(geometry.center + direction * outerRadius)
- points.append(numpy.array([float("nan"), float("nan")]))
- points = numpy.array(points)
- else:
- innerRadius = geometry.radius - geometry.weight * 0.5
- outerRadius = geometry.radius + geometry.weight * 0.5
-
- delta = 0.1 if geometry.endAngle >= geometry.startAngle else -0.1
- if geometry.startAngle == geometry.endAngle:
- # Degenerated, it's a line (single radius)
- angle = geometry.startAngle
- direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
- points = []
- points.append(geometry.center + direction * innerRadius)
- points.append(geometry.center + direction * outerRadius)
- return numpy.array(points)
-
- angles = numpy.arange(geometry.startAngle, geometry.endAngle, delta)
- if angles[-1] != geometry.endAngle:
- angles = numpy.append(angles, geometry.endAngle)
-
- if kind == "camembert":
- # It's a part of camembert
- points = []
- points.append(geometry.center)
- points.append(geometry.startPoint)
- delta = 0.1 if geometry.endAngle >= geometry.startAngle else -0.1
- for angle in angles:
- direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
- points.append(geometry.center + direction * outerRadius)
- points.append(geometry.endPoint)
- points.append(geometry.center)
- elif kind == "arc":
- # It's a part of donut
- points = []
- points.append(geometry.startPoint)
- for angle in angles:
- direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
- points.insert(0, geometry.center + direction * innerRadius)
- points.append(geometry.center + direction * outerRadius)
- points.insert(0, geometry.endPoint)
- points.append(geometry.endPoint)
- else:
- assert False
-
- points = numpy.array(points)
-
- return points
-
- def _updateShape(self):
- geometry = self._geometry
- points = self._createShapeFromGeometry(geometry)
- self.__shape.setPoints(points)
-
- index = numpy.nanargmin(points[:, 1])
- pos = points[index]
- with utils.blockSignals(self._handleLabel):
- self._handleLabel.setPosition(pos[0], pos[1])
-
- if geometry.center is None:
- movePos = geometry.startPoint * 0.34 + geometry.endPoint * 0.66
- else:
- movePos = geometry.center
-
- with utils.blockSignals(self._handleMove):
- self._handleMove.setPosition(*movePos)
-
- self.sigRegionChanged.emit()
-
- def getGeometry(self):
- """Returns a tuple containing the geometry of this ROI
-
- It is a symmetric function of :meth:`setGeometry`.
-
- If `startAngle` is smaller than `endAngle` the rotation is clockwise,
- else the rotation is anticlockwise.
-
- :rtype: Tuple[numpy.ndarray,float,float,float,float]
- :raise ValueError: In case the ROI can't be represented as section of
- a circle
- """
- geometry = self._geometry
- if geometry.center is None:
- raise ValueError("This ROI can't be represented as a section of circle")
- return geometry.center, self.getInnerRadius(), self.getOuterRadius(), geometry.startAngle, geometry.endAngle
-
- def isClosed(self):
- """Returns true if the arc is a closed shape, like a circle or a donut.
-
- :rtype: bool
- """
- return self._geometry.isClosed()
-
- def getCenter(self):
- """Returns the center of the circle used to draw arcs of this ROI.
-
- This center is usually outside the the shape itself.
-
- :rtype: numpy.ndarray
- """
- return self._geometry.center
-
- def getStartAngle(self):
- """Returns the angle of the start of the section of this ROI (in radian).
-
- If `startAngle` is smaller than `endAngle` the rotation is clockwise,
- else the rotation is anticlockwise.
-
- :rtype: float
- """
- return self._geometry.startAngle
-
- def getEndAngle(self):
- """Returns the angle of the end of the section of this ROI (in radian).
-
- If `startAngle` is smaller than `endAngle` the rotation is clockwise,
- else the rotation is anticlockwise.
-
- :rtype: float
- """
- return self._geometry.endAngle
-
- def getInnerRadius(self):
- """Returns the radius of the smaller arc used to draw this ROI.
-
- :rtype: float
- """
- geometry = self._geometry
- radius = geometry.radius - geometry.weight * 0.5
- if radius < 0:
- radius = 0
- return radius
-
- def getOuterRadius(self):
- """Returns the radius of the bigger arc used to draw this ROI.
-
- :rtype: float
- """
- geometry = self._geometry
- radius = geometry.radius + geometry.weight * 0.5
- return radius
-
- def setGeometry(self, center, innerRadius, outerRadius, startAngle, endAngle):
- """
- Set the geometry of this arc.
-
- :param numpy.ndarray center: Center of the circle.
- :param float innerRadius: Radius of the smaller arc of the section.
- :param float outerRadius: Weight of the bigger arc of the section.
- It have to be bigger than `innerRadius`
- :param float startAngle: Location of the start of the section (in radian)
- :param float endAngle: Location of the end of the section (in radian).
- If `startAngle` is smaller than `endAngle` the rotation is clockwise,
- else the rotation is anticlockwise.
- """
- if innerRadius > outerRadius:
- logger.error("inner radius larger than outer radius")
- innerRadius, outerRadius = outerRadius, innerRadius
- center = numpy.array(center)
- radius = (innerRadius + outerRadius) * 0.5
- weight = outerRadius - innerRadius
-
- vector = numpy.array([numpy.cos(startAngle), numpy.sin(startAngle)])
- startPoint = center + vector * radius
- vector = numpy.array([numpy.cos(endAngle), numpy.sin(endAngle)])
- endPoint = center + vector * radius
-
- geometry = _ArcGeometry(center, startPoint, endPoint,
- radius, weight,
- startAngle, endAngle, closed=None)
- self._geometry = geometry
- self._updateHandles()
-
- @docstring(HandleBasedROI)
- def contains(self, position):
- # first check distance, fastest
- center = self.getCenter()
- distance = numpy.sqrt((position[1] - center[1]) ** 2 + ((position[0] - center[0])) ** 2)
- is_in_distance = self.getInnerRadius() <= distance <= self.getOuterRadius()
- if not is_in_distance:
- return False
- rel_pos = position[1] - center[1], position[0] - center[0]
- angle = numpy.arctan2(*rel_pos)
- # angle is inside [-pi, pi]
-
- # Normalize the start angle between [-pi, pi]
- # with a positive angle range
- start_angle = self.getStartAngle()
- end_angle = self.getEndAngle()
- azim_range = end_angle - start_angle
- if azim_range < 0:
- start_angle = end_angle
- azim_range = -azim_range
- start_angle = numpy.mod(start_angle + numpy.pi, 2 * numpy.pi) - numpy.pi
-
- if angle < start_angle:
- angle += 2 * numpy.pi
- return start_angle <= angle <= start_angle + azim_range
-
- def translate(self, x, y):
- self._geometry = self._geometry.translated(x, y)
- self._updateHandles()
-
- def _arcCurvatureMarkerConstraint(self, x, y):
- """Curvature marker remains on perpendicular bisector"""
- geometry = self._geometry
- if geometry.center is None:
- center = (geometry.startPoint + geometry.endPoint) * 0.5
- vector = geometry.startPoint - geometry.endPoint
- vector = numpy.array((vector[1], -vector[0]))
- vdist = numpy.linalg.norm(vector)
- if vdist != 0:
- normal = numpy.array((vector[1], -vector[0])) / vdist
- else:
- normal = numpy.array((0, 0))
- else:
- if geometry.isClosed():
- midAngle = geometry.startAngle + numpy.pi * 0.5
- else:
- midAngle = (geometry.startAngle + geometry.endAngle) * 0.5
- normal = numpy.array([numpy.cos(midAngle), numpy.sin(midAngle)])
- center = geometry.center
- dist = numpy.dot(normal, (numpy.array((x, y)) - center))
- dist = numpy.clip(dist, geometry.radius, geometry.radius * 2)
- x, y = center + dist * normal
- return x, y
-
- @staticmethod
- def _circleEquation(pt1, pt2, pt3):
- """Circle equation from 3 (x, y) points
-
- :return: Position of the center of the circle and the radius
- :rtype: Tuple[Tuple[float,float],float]
- """
- x, y, z = complex(*pt1), complex(*pt2), complex(*pt3)
- w = z - x
- w /= y - x
- c = (x - y) * (w - abs(w) ** 2) / 2j / w.imag - x
- return numpy.array((-c.real, -c.imag)), abs(c + x)
-
- def __str__(self):
- try:
- center, innerRadius, outerRadius, startAngle, endAngle = self.getGeometry()
- params = center[0], center[1], innerRadius, outerRadius, startAngle, endAngle
- params = 'center: %f %f; radius: %f %f; angles: %f %f' % params
- except ValueError:
- params = "invalid"
- return "%s(%s)" % (self.__class__.__name__, params)
diff --git a/silx/gui/plot/items/_pick.py b/silx/gui/plot/items/_pick.py
deleted file mode 100644
index 8c8e781..0000000
--- a/silx/gui/plot/items/_pick.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2019-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""This module provides classes supporting item picking."""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "04/06/2019"
-
-import numpy
-
-
-class PickingResult(object):
- """Class to access picking information in a :class:`PlotWidget`"""
-
- def __init__(self, item, indices=None):
- """Init
-
- :param item: The picked item
- :param numpy.ndarray indices: Array-like of indices of picked data.
- Either 1D or 2D with dim0: data dimension and dim1: indices.
- No copy is made.
- """
- self._item = item
-
- if indices is None or len(indices) == 0:
- self._indices = None
- else:
- # Indices is set to None if indices array is empty
- indices = numpy.array(indices, copy=False, dtype=numpy.int64)
- self._indices = None if indices.size == 0 else indices
-
- def getItem(self):
- """Returns the item this results corresponds to."""
- return self._item
-
- def getIndices(self, copy=True):
- """Returns indices of picked data.
-
- If data is 1D, it returns a numpy.ndarray, otherwise
- it returns a tuple with as many numpy.ndarray as there are
- dimensions in the data.
-
- :param bool copy: True (default) to get a copy,
- False to return internal arrays
- :rtype: Union[None,numpy.ndarray,List[numpy.ndarray]]
- """
- if self._indices is None:
- return None
- indices = numpy.array(self._indices, copy=copy)
- return indices if indices.ndim == 1 else tuple(indices)
diff --git a/silx/gui/plot/items/_roi_base.py b/silx/gui/plot/items/_roi_base.py
deleted file mode 100644
index 3eb6cf4..0000000
--- a/silx/gui/plot/items/_roi_base.py
+++ /dev/null
@@ -1,835 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""This module provides base components to create ROI item for
-the :class:`~silx.gui.plot.PlotWidget`.
-
-.. inheritance-diagram::
- silx.gui.plot.items.roi
- :parts: 1
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "28/06/2018"
-
-
-import logging
-import numpy
-import weakref
-
-from ....utils.weakref import WeakList
-from ... import qt
-from .. import items
-from ..items import core
-from ...colors import rgba
-import silx.utils.deprecation
-from ....utils.proxy import docstring
-
-
-logger = logging.getLogger(__name__)
-
-
-class _RegionOfInterestBase(qt.QObject):
- """Base class of 1D and 2D region of interest
-
- :param QObject parent: See QObject
- :param str name: The name of the ROI
- """
-
- sigAboutToBeRemoved = qt.Signal()
- """Signal emitted just before this ROI is removed from its manager."""
-
- sigItemChanged = qt.Signal(object)
- """Signal emitted when item has changed.
-
- It provides a flag describing which property of the item has changed.
- See :class:`ItemChangedType` for flags description.
- """
-
- def __init__(self, parent=None):
- qt.QObject.__init__(self, parent=parent)
- self.__name = ''
-
- def getName(self):
- """Returns the name of the ROI
-
- :return: name of the region of interest
- :rtype: str
- """
- return self.__name
-
- def setName(self, name):
- """Set the name of the ROI
-
- :param str name: name of the region of interest
- """
- name = str(name)
- if self.__name != name:
- self.__name = name
- self._updated(items.ItemChangedType.NAME)
-
- def _updated(self, event=None, checkVisibility=True):
- """Implement Item mix-in update method by updating the plot items
-
- See :class:`~silx.gui.plot.items.Item._updated`
- """
- self.sigItemChanged.emit(event)
-
- def contains(self, position):
- """Returns True if the `position` is in this ROI.
-
- :param tuple[float,float] position: position to check
- :return: True if the value / point is consider to be in the region of
- interest.
- :rtype: bool
- """
- return False # Override in subclass to perform actual test
-
-
-class RoiInteractionMode(object):
- """Description of an interaction mode.
-
- An interaction mode provide a specific kind of interaction for a ROI.
- A ROI can implement many interaction.
- """
-
- def __init__(self, label, description=None):
- self._label = label
- self._description = description
-
- @property
- def label(self):
- return self._label
-
- @property
- def description(self):
- return self._description
-
-
-class InteractionModeMixIn(object):
- """Mix in feature which can be implemented by a ROI object.
-
- This provides user interaction to switch between different
- interaction mode to edit the ROI.
-
- This ROI modes have to be described using `RoiInteractionMode`,
- and taken into account during interation with handles.
- """
-
- sigInteractionModeChanged = qt.Signal(object)
-
- def __init__(self):
- self.__modeId = None
-
- def _initInteractionMode(self, modeId):
- """Set the mode without updating anything.
-
- Must be one of the returned :meth:`availableInteractionModes`.
-
- :param RoiInteractionMode modeId: Mode to use
- """
- self.__modeId = modeId
-
- def availableInteractionModes(self):
- """Returns the list of available interaction modes
-
- Must be implemented when inherited to provide all available modes.
-
- :rtype: List[RoiInteractionMode]
- """
- raise NotImplementedError()
-
- def setInteractionMode(self, modeId):
- """Set the interaction mode.
-
- :param RoiInteractionMode modeId: Mode to use
- """
- self.__modeId = modeId
- self._interactiveModeUpdated(modeId)
- self.sigInteractionModeChanged.emit(modeId)
-
- def _interactiveModeUpdated(self, modeId):
- """Called directly after an update of the mode.
-
- The signal `sigInteractionModeChanged` is triggered after this
- call.
-
- Must be implemented when inherited to take care of the change.
- """
- raise NotImplementedError()
-
- def getInteractionMode(self):
- """Returns the interaction mode.
-
- Must be one of the returned :meth:`availableInteractionModes`.
-
- :rtype: RoiInteractionMode
- """
- return self.__modeId
-
-
-class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
- """Object describing a region of interest in a plot.
-
- :param QObject parent:
- The RegionOfInterestManager that created this object
- """
-
- _DEFAULT_LINEWIDTH = 1.
- """Default line width of the curve"""
-
- _DEFAULT_LINESTYLE = '-'
- """Default line style of the curve"""
-
- _DEFAULT_HIGHLIGHT_STYLE = items.CurveStyle(linewidth=2)
- """Default highlight style of the item"""
-
- ICON, NAME, SHORT_NAME = None, None, None
- """Metadata to describe the ROI in labels, tooltips and widgets
-
- Should be set by inherited classes to custom the ROI manager widget.
- """
-
- sigRegionChanged = qt.Signal()
- """Signal emitted everytime the shape or position of the ROI changes"""
-
- sigEditingStarted = qt.Signal()
- """Signal emitted when the user start editing the roi"""
-
- sigEditingFinished = qt.Signal()
- """Signal emitted when the region edition is finished. During edition
- sigEditionChanged will be emitted several times and
- sigRegionEditionFinished only at end"""
-
- def __init__(self, parent=None):
- # Avoid circular dependency
- from ..tools import roi as roi_tools
- assert parent is None or isinstance(parent, roi_tools.RegionOfInterestManager)
- _RegionOfInterestBase.__init__(self, parent)
- core.HighlightedMixIn.__init__(self)
- self._color = rgba('red')
- self._editable = False
- self._selectable = False
- self._focusProxy = None
- self._visible = True
- self._child = WeakList()
-
- def _connectToPlot(self, plot):
- """Called after connection to a plot"""
- for item in self.getItems():
- # This hack is needed to avoid reentrant call from _disconnectFromPlot
- # to the ROI manager. It also speed up the item tests in _itemRemoved
- item._roiGroup = True
- plot.addItem(item)
-
- def _disconnectFromPlot(self, plot):
- """Called before disconnection from a plot"""
- for item in self.getItems():
- # The item could be already be removed by the plot
- if item.getPlot() is not None:
- del item._roiGroup
- plot.removeItem(item)
-
- def _setItemName(self, item):
- """Helper to generate a unique id to a plot item"""
- legend = "__ROI-%d__%d" % (id(self), id(item))
- item.setName(legend)
-
- def setParent(self, parent):
- """Set the parent of the RegionOfInterest
-
- :param Union[None,RegionOfInterestManager] parent: The new parent
- """
- # Avoid circular dependency
- from ..tools import roi as roi_tools
- if (parent is not None and not isinstance(parent, roi_tools.RegionOfInterestManager)):
- raise ValueError('Unsupported parent')
-
- previousParent = self.parent()
- if previousParent is not None:
- previousPlot = previousParent.parent()
- if previousPlot is not None:
- self._disconnectFromPlot(previousPlot)
- super(RegionOfInterest, self).setParent(parent)
- if parent is not None:
- plot = parent.parent()
- if plot is not None:
- self._connectToPlot(plot)
-
- def addItem(self, item):
- """Add an item to the set of this ROI children.
-
- This item will be added and removed to the plot used by the ROI.
-
- If the ROI is already part of a plot, the item will also be added to
- the plot.
-
- It the item do not have a name already, a unique one is generated to
- avoid item collision in the plot.
-
- :param silx.gui.plot.items.Item item: A plot item
- """
- assert item is not None
- self._child.append(item)
- if item.getName() == '':
- self._setItemName(item)
- manager = self.parent()
- if manager is not None:
- plot = manager.parent()
- if plot is not None:
- item._roiGroup = True
- plot.addItem(item)
-
- def removeItem(self, item):
- """Remove an item from this ROI children.
-
- If the item is part of a plot it will be removed too.
-
- :param silx.gui.plot.items.Item item: A plot item
- """
- assert item is not None
- self._child.remove(item)
- plot = item.getPlot()
- if plot is not None:
- del item._roiGroup
- plot.removeItem(item)
-
- def getItems(self):
- """Returns the list of PlotWidget items of this RegionOfInterest.
-
- :rtype: List[~silx.gui.plot.items.Item]
- """
- return tuple(self._child)
-
- @classmethod
- def _getShortName(cls):
- """Return an human readable kind of ROI
-
- :rtype: str
- """
- if hasattr(cls, "SHORT_NAME"):
- name = cls.SHORT_NAME
- if name is None:
- name = cls.__name__
- return name
-
- def getColor(self):
- """Returns the color of this ROI
-
- :rtype: QColor
- """
- return qt.QColor.fromRgbF(*self._color)
-
- def setColor(self, color):
- """Set the color used for this ROI.
-
- :param color: The color to use for ROI shape as
- either a color name, a QColor, a list of uint8 or float in [0, 1].
- """
- color = rgba(color)
- if color != self._color:
- self._color = color
- self._updated(items.ItemChangedType.COLOR)
-
- @silx.utils.deprecation.deprecated(reason='API modification',
- replacement='getName()',
- since_version=0.12)
- def getLabel(self):
- """Returns the label displayed for this ROI.
-
- :rtype: str
- """
- return self.getName()
-
- @silx.utils.deprecation.deprecated(reason='API modification',
- replacement='setName(name)',
- since_version=0.12)
- def setLabel(self, label):
- """Set the label displayed with this ROI.
-
- :param str label: The text label to display
- """
- self.setName(name=label)
-
- def isEditable(self):
- """Returns whether the ROI is editable by the user or not.
-
- :rtype: bool
- """
- return self._editable
-
- def setEditable(self, editable):
- """Set whether the ROI can be changed interactively.
-
- :param bool editable: True to allow edition by the user,
- False to disable.
- """
- editable = bool(editable)
- if self._editable != editable:
- self._editable = editable
- self._updated(items.ItemChangedType.EDITABLE)
-
- def isSelectable(self):
- """Returns whether the ROI is selectable by the user or not.
-
- :rtype: bool
- """
- return self._selectable
-
- def setSelectable(self, selectable):
- """Set whether the ROI can be selected interactively.
-
- :param bool selectable: True to allow selection by the user,
- False to disable.
- """
- selectable = bool(selectable)
- if self._selectable != selectable:
- self._selectable = selectable
- self._updated(items.ItemChangedType.SELECTABLE)
-
- def getFocusProxy(self):
- """Returns the ROI which have to be selected when this ROI is selected,
- else None if no proxy specified.
-
- :rtype: RegionOfInterest
- """
- proxy = self._focusProxy
- if proxy is None:
- return None
- proxy = proxy()
- if proxy is None:
- self._focusProxy = None
- return proxy
-
- def setFocusProxy(self, roi):
- """Set the real ROI which will be selected when this ROI is selected,
- else None to remove the proxy already specified.
-
- :param RegionOfInterest roi: A ROI
- """
- if roi is not None:
- self._focusProxy = weakref.ref(roi)
- else:
- self._focusProxy = None
-
- def isVisible(self):
- """Returns whether the ROI is visible in the plot.
-
- .. note::
- This does not take into account whether or not the plot
- widget itself is visible (unlike :meth:`QWidget.isVisible` which
- checks the visibility of all its parent widgets up to the window)
-
- :rtype: bool
- """
- return self._visible
-
- def setVisible(self, visible):
- """Set whether the plot items associated with this ROI are
- visible in the plot.
-
- :param bool visible: True to show the ROI in the plot, False to
- hide it.
- """
- visible = bool(visible)
- if self._visible != visible:
- self._visible = visible
- self._updated(items.ItemChangedType.VISIBLE)
-
- @classmethod
- def showFirstInteractionShape(cls):
- """Returns True if the shape created by the first interaction and
- managed by the plot have to be visible.
-
- :rtype: bool
- """
- return False
-
- @classmethod
- def getFirstInteractionShape(cls):
- """Returns the shape kind which will be used by the very first
- interaction with the plot.
-
- This interactions are hardcoded inside the plot
-
- :rtype: str
- """
- return cls._plotShape
-
- def setFirstShapePoints(self, points):
- """"Initialize the ROI using the points from the first interaction.
-
- This interaction is constrained by the plot API and only supports few
- shapes.
- """
- raise NotImplementedError()
-
- def creationStarted(self):
- """"Called when the ROI creation interaction was started.
- """
- pass
-
- def creationFinalized(self):
- """"Called when the ROI creation interaction was finalized.
- """
- pass
-
- def _updateItemProperty(self, event, source, destination):
- """Update the item property of a destination from an item source.
-
- :param items.ItemChangedType event: Property type to update
- :param silx.gui.plot.items.Item source: The reference for the data
- :param event Union[Item,List[Item]] destination: The item(s) to update
- """
- if not isinstance(destination, (list, tuple)):
- destination = [destination]
- if event == items.ItemChangedType.NAME:
- value = source.getName()
- for d in destination:
- d.setName(value)
- elif event == items.ItemChangedType.EDITABLE:
- value = source.isEditable()
- for d in destination:
- d.setEditable(value)
- elif event == items.ItemChangedType.SELECTABLE:
- value = source.isSelectable()
- for d in destination:
- d._setSelectable(value)
- elif event == items.ItemChangedType.COLOR:
- value = rgba(source.getColor())
- for d in destination:
- d.setColor(value)
- elif event == items.ItemChangedType.LINE_STYLE:
- value = self.getLineStyle()
- for d in destination:
- d.setLineStyle(value)
- elif event == items.ItemChangedType.LINE_WIDTH:
- value = self.getLineWidth()
- for d in destination:
- d.setLineWidth(value)
- elif event == items.ItemChangedType.SYMBOL:
- value = self.getSymbol()
- for d in destination:
- d.setSymbol(value)
- elif event == items.ItemChangedType.SYMBOL_SIZE:
- value = self.getSymbolSize()
- for d in destination:
- d.setSymbolSize(value)
- elif event == items.ItemChangedType.VISIBLE:
- value = self.isVisible()
- for d in destination:
- d.setVisible(value)
- else:
- assert False
-
- def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.HIGHLIGHTED:
- style = self.getCurrentStyle()
- self._updatedStyle(event, style)
- else:
- styleEvents = [items.ItemChangedType.COLOR,
- items.ItemChangedType.LINE_STYLE,
- items.ItemChangedType.LINE_WIDTH,
- items.ItemChangedType.SYMBOL,
- items.ItemChangedType.SYMBOL_SIZE]
- if self.isHighlighted():
- styleEvents.append(items.ItemChangedType.HIGHLIGHTED_STYLE)
-
- if event in styleEvents:
- style = self.getCurrentStyle()
- self._updatedStyle(event, style)
-
- super(RegionOfInterest, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- """Called when the current displayed style of the ROI was changed.
-
- :param event: The event responsible of the change of the style
- :param items.CurveStyle style: The current style
- """
- pass
-
- def getCurrentStyle(self):
- """Returns the current curve style.
-
- Curve style depends on curve highlighting
-
- :rtype: CurveStyle
- """
- baseColor = rgba(self.getColor())
- if isinstance(self, core.LineMixIn):
- baseLinestyle = self.getLineStyle()
- baseLinewidth = self.getLineWidth()
- else:
- baseLinestyle = self._DEFAULT_LINESTYLE
- baseLinewidth = self._DEFAULT_LINEWIDTH
- if isinstance(self, core.SymbolMixIn):
- baseSymbol = self.getSymbol()
- baseSymbolsize = self.getSymbolSize()
- else:
- baseSymbol = 'o'
- baseSymbolsize = 1
-
- if self.isHighlighted():
- style = self.getHighlightedStyle()
- color = style.getColor()
- linestyle = style.getLineStyle()
- linewidth = style.getLineWidth()
- symbol = style.getSymbol()
- symbolsize = style.getSymbolSize()
-
- return items.CurveStyle(
- color=baseColor if color is None else color,
- linestyle=baseLinestyle if linestyle is None else linestyle,
- linewidth=baseLinewidth if linewidth is None else linewidth,
- symbol=baseSymbol if symbol is None else symbol,
- symbolsize=baseSymbolsize if symbolsize is None else symbolsize)
- else:
- return items.CurveStyle(color=baseColor,
- linestyle=baseLinestyle,
- linewidth=baseLinewidth,
- symbol=baseSymbol,
- symbolsize=baseSymbolsize)
-
- def _editingStarted(self):
- assert self._editable is True
- self.sigEditingStarted.emit()
-
- def _editingFinished(self):
- self.sigEditingFinished.emit()
-
-
-class HandleBasedROI(RegionOfInterest):
- """Manage a ROI based on a set of handles"""
-
- def __init__(self, parent=None):
- RegionOfInterest.__init__(self, parent=parent)
- self._handles = []
- self._posOrigin = None
- self._posPrevious = None
-
- def addUserHandle(self, item=None):
- """
- Add a new free handle to the ROI.
-
- This handle do nothing. It have to be managed by the ROI
- implementing this class.
-
- :param Union[None,silx.gui.plot.items.Marker] item: The new marker to
- add, else None to create a default marker.
- :rtype: silx.gui.plot.items.Marker
- """
- return self.addHandle(item, role="user")
-
- def addLabelHandle(self, item=None):
- """
- Add a new label handle to the ROI.
-
- This handle is not draggable nor selectable.
-
- It is displayed without symbol, but it is always visible anyway
- the ROI is editable, in order to display text.
-
- :param Union[None,silx.gui.plot.items.Marker] item: The new marker to
- add, else None to create a default marker.
- :rtype: silx.gui.plot.items.Marker
- """
- return self.addHandle(item, role="label")
-
- def addTranslateHandle(self, item=None):
- """
- Add a new translate handle to the ROI.
-
- Dragging translate handles affect the position position of the ROI
- but not the shape itself.
-
- :param Union[None,silx.gui.plot.items.Marker] item: The new marker to
- add, else None to create a default marker.
- :rtype: silx.gui.plot.items.Marker
- """
- return self.addHandle(item, role="translate")
-
- def addHandle(self, item=None, role="default"):
- """
- Add a new handle to the ROI.
-
- Dragging handles while affect the position or the shape of the
- ROI.
-
- :param Union[None,silx.gui.plot.items.Marker] item: The new marker to
- add, else None to create a default marker.
- :rtype: silx.gui.plot.items.Marker
- """
- if item is None:
- item = items.Marker()
- color = rgba(self.getColor())
- color = self._computeHandleColor(color)
- item.setColor(color)
- if role == "default":
- item.setSymbol("s")
- elif role == "user":
- pass
- elif role == "translate":
- item.setSymbol("+")
- elif role == "label":
- item.setSymbol("")
-
- if role == "user":
- pass
- elif role == "label":
- item._setSelectable(False)
- item._setDraggable(False)
- item.setVisible(True)
- else:
- self.__updateEditable(item, self.isEditable(), remove=False)
- item._setSelectable(False)
-
- self._handles.append((item, role))
- self.addItem(item)
- return item
-
- def removeHandle(self, handle):
- data = [d for d in self._handles if d[0] is handle][0]
- self._handles.remove(data)
- role = data[1]
- if role not in ["user", "label"]:
- if self.isEditable():
- self.__updateEditable(handle, False)
- self.removeItem(handle)
-
- def getHandles(self):
- """Returns the list of handles of this HandleBasedROI.
-
- :rtype: List[~silx.gui.plot.items.Marker]
- """
- return tuple(data[0] for data in self._handles)
-
- def _updated(self, event=None, checkVisibility=True):
- """Implement Item mix-in update method by updating the plot items
-
- See :class:`~silx.gui.plot.items.Item._updated`
- """
- if event == items.ItemChangedType.NAME:
- self._updateText(self.getName())
- elif event == items.ItemChangedType.VISIBLE:
- for item, role in self._handles:
- visible = self.isVisible()
- editionVisible = visible and self.isEditable()
- if role not in ["user", "label"]:
- item.setVisible(editionVisible)
- else:
- item.setVisible(visible)
- elif event == items.ItemChangedType.EDITABLE:
- for item, role in self._handles:
- editable = self.isEditable()
- if role not in ["user", "label"]:
- self.__updateEditable(item, editable)
- super(HandleBasedROI, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- super(HandleBasedROI, self)._updatedStyle(event, style)
-
- # Update color of shape items in the plot
- color = rgba(self.getColor())
- handleColor = self._computeHandleColor(color)
- for item, role in self._handles:
- if role == 'user':
- pass
- elif role == 'label':
- item.setColor(color)
- else:
- item.setColor(handleColor)
-
- def __updateEditable(self, handle, editable, remove=True):
- # NOTE: visibility change emit a position update event
- handle.setVisible(editable and self.isVisible())
- handle._setDraggable(editable)
- if editable:
- handle.sigDragStarted.connect(self._handleEditingStarted)
- handle.sigItemChanged.connect(self._handleEditingUpdated)
- handle.sigDragFinished.connect(self._handleEditingFinished)
- else:
- if remove:
- handle.sigDragStarted.disconnect(self._handleEditingStarted)
- handle.sigItemChanged.disconnect(self._handleEditingUpdated)
- handle.sigDragFinished.disconnect(self._handleEditingFinished)
-
- def _handleEditingStarted(self):
- super(HandleBasedROI, self)._editingStarted()
- handle = self.sender()
- self._posOrigin = numpy.array(handle.getPosition())
- self._posPrevious = numpy.array(self._posOrigin)
- self.handleDragStarted(handle, self._posOrigin)
-
- def _handleEditingUpdated(self):
- if self._posOrigin is None:
- # Avoid to handle events when visibility change
- return
- handle = self.sender()
- current = numpy.array(handle.getPosition())
- self.handleDragUpdated(handle, self._posOrigin, self._posPrevious, current)
- self._posPrevious = current
-
- def _handleEditingFinished(self):
- handle = self.sender()
- current = numpy.array(handle.getPosition())
- self.handleDragFinished(handle, self._posOrigin, current)
- self._posPrevious = None
- self._posOrigin = None
- super(HandleBasedROI, self)._editingFinished()
-
- def isHandleBeingDragged(self):
- """Returns True if one of the handles is currently being dragged.
-
- :rtype: bool
- """
- return self._posOrigin is not None
-
- def handleDragStarted(self, handle, origin):
- """Called when an handler drag started"""
- pass
-
- def handleDragUpdated(self, handle, origin, previous, current):
- """Called when an handle drag position changed"""
- pass
-
- def handleDragFinished(self, handle, origin, current):
- """Called when an handle drag finished"""
- pass
-
- def _computeHandleColor(self, color):
- """Returns the anchor color from the base ROI color
-
- :param Union[numpy.array,Tuple,List]: color
- :rtype: Union[numpy.array,Tuple,List]
- """
- return color[:3] + (0.5,)
-
- def _updateText(self, text):
- """Update the text displayed by this ROI
-
- :param str text: A text
- """
- pass
diff --git a/silx/gui/plot/items/axis.py b/silx/gui/plot/items/axis.py
deleted file mode 100644
index be85e6a..0000000
--- a/silx/gui/plot/items/axis.py
+++ /dev/null
@@ -1,569 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""This module provides the class for axes of the :class:`PlotWidget`.
-"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "22/11/2018"
-
-import datetime as dt
-import enum
-import logging
-
-import dateutil.tz
-
-from ... import qt
-
-
-_logger = logging.getLogger(__name__)
-
-
-class TickMode(enum.Enum):
- """Determines if ticks are regular number or datetimes."""
- DEFAULT = 0 # Ticks are regular numbers
- TIME_SERIES = 1 # Ticks are datetime objects
-
-
-class Axis(qt.QObject):
- """This class describes and controls a plot axis.
-
- Note: This is an abstract class.
- """
- # States are half-stored on the backend of the plot, and half-stored on this
- # object.
- # TODO It would be good to store all the states of an axis in this object.
- # i.e. vmin and vmax
-
- LINEAR = "linear"
- """Constant defining a linear scale"""
-
- LOGARITHMIC = "log"
- """Constant defining a logarithmic scale"""
-
- _SCALES = set([LINEAR, LOGARITHMIC])
-
- sigInvertedChanged = qt.Signal(bool)
- """Signal emitted when axis orientation has changed"""
-
- sigScaleChanged = qt.Signal(str)
- """Signal emitted when axis scale has changed"""
-
- _sigLogarithmicChanged = qt.Signal(bool)
- """Signal emitted when axis scale has changed to or from logarithmic"""
-
- sigAutoScaleChanged = qt.Signal(bool)
- """Signal emitted when axis autoscale has changed"""
-
- sigLimitsChanged = qt.Signal(float, float)
- """Signal emitted when axis limits have changed"""
-
- def __init__(self, plot):
- """Constructor
-
- :param silx.gui.plot.PlotWidget.PlotWidget plot: Parent plot of this
- axis
- """
- qt.QObject.__init__(self, parent=plot)
- self._scale = self.LINEAR
- self._isAutoScale = True
- # Store default labels provided to setGraph[X|Y]Label
- self._defaultLabel = ''
- # Store currently displayed labels
- # Current label can differ from input one with active curve handling
- self._currentLabel = ''
-
- def _getPlot(self):
- """Returns the PlotWidget this Axis belongs to.
-
- :rtype: PlotWidget
- """
- plot = self.parent()
- if plot is None:
- raise RuntimeError("Axis no longer attached to a PlotWidget")
- return plot
-
- def _getBackend(self):
- """Returns the backend
-
- :rtype: BackendBase
- """
- return self._getPlot()._backend
-
- def getLimits(self):
- """Get the limits of this axis.
-
- :return: Minimum and maximum values of this axis as tuple
- """
- return self._internalGetLimits()
-
- def setLimits(self, vmin, vmax):
- """Set this axis limits.
-
- :param float vmin: minimum axis value
- :param float vmax: maximum axis value
- """
- vmin, vmax = self._checkLimits(vmin, vmax)
- if self.getLimits() == (vmin, vmax):
- return
-
- self._internalSetLimits(vmin, vmax)
- self._getPlot()._setDirtyPlot()
-
- self._emitLimitsChanged()
-
- def _emitLimitsChanged(self):
- """Emit axis sigLimitsChanged and PlotWidget limitsChanged event"""
- vmin, vmax = self.getLimits()
- self.sigLimitsChanged.emit(vmin, vmax)
- self._getPlot()._notifyLimitsChanged(emitSignal=False)
-
- def _checkLimits(self, vmin, vmax):
- """Makes sure axis range is not empty
-
- :param float vmin: Min axis value
- :param float vmax: Max axis value
- :return: (min, max) making sure min < max
- :rtype: 2-tuple of float
- """
- if vmax < vmin:
- _logger.debug('%s axis: max < min, inverting limits.', self._defaultLabel)
- vmin, vmax = vmax, vmin
- elif vmax == vmin:
- _logger.debug('%s axis: max == min, expanding limits.', self._defaultLabel)
- if vmin == 0.:
- vmin, vmax = -0.1, 0.1
- elif vmin < 0:
- vmin, vmax = vmin * 1.1, vmin * 0.9
- else: # xmin > 0
- vmin, vmax = vmin * 0.9, vmin * 1.1
-
- return vmin, vmax
-
- def isInverted(self):
- """Return True if the axis is inverted (top to bottom for the y-axis),
- False otherwise. It is always False for the X axis.
-
- :rtype: bool
- """
- return False
-
- def setInverted(self, isInverted):
- """Set the axis orientation.
-
- This is only available for the Y axis.
-
- :param bool flag: True for Y axis going from top to bottom,
- False for Y axis going from bottom to top
- """
- if isInverted == self.isInverted():
- return
- raise NotImplementedError()
-
- def getLabel(self):
- """Return the current displayed label of this axis.
-
- :param str axis: The Y axis for which to get the label (left or right)
- :rtype: str
- """
- return self._currentLabel
-
- def setLabel(self, label):
- """Set the label displayed on the plot for this axis.
-
- The provided label can be temporarily replaced by the label of the
- active curve if any.
-
- :param str label: The axis label
- """
- self._defaultLabel = label
- self._setCurrentLabel(label)
- self._getPlot()._setDirtyPlot()
-
- def _setCurrentLabel(self, label):
- """Define the label currently displayed.
-
- If the label is None or empty the default label is used.
-
- :param str label: Currently displayed label
- """
- if label is None or label == '':
- label = self._defaultLabel
- if label is None:
- label = ''
- self._currentLabel = label
- self._internalSetCurrentLabel(label)
-
- def getScale(self):
- """Return the name of the scale used by this axis.
-
- :rtype: str
- """
- return self._scale
-
- def setScale(self, scale):
- """Set the scale to be used by this axis.
-
- :param str scale: Name of the scale ("log", or "linear")
- """
- assert(scale in self._SCALES)
- if self._scale == scale:
- return
-
- # For the backward compatibility signal
- emitLog = self._scale == self.LOGARITHMIC or scale == self.LOGARITHMIC
-
- self._scale = scale
-
- # TODO hackish way of forcing update of curves and images
- plot = self._getPlot()
- for item in plot.getItems():
- item._updated()
- plot._invalidateDataRange()
-
- if scale == self.LOGARITHMIC:
- self._internalSetLogarithmic(True)
- elif scale == self.LINEAR:
- self._internalSetLogarithmic(False)
- else:
- raise ValueError("Scale %s unsupported" % scale)
-
- plot._forceResetZoom()
-
- self.sigScaleChanged.emit(self._scale)
- if emitLog:
- self._sigLogarithmicChanged.emit(self._scale == self.LOGARITHMIC)
-
- def _isLogarithmic(self):
- """Return True if this axis scale is logarithmic, False if linear.
-
- :rtype: bool
- """
- return self._scale == self.LOGARITHMIC
-
- def _setLogarithmic(self, flag):
- """Set the scale of this axes (either linear or logarithmic).
-
- :param bool flag: True to use a logarithmic scale, False for linear.
- """
- flag = bool(flag)
- self.setScale(self.LOGARITHMIC if flag else self.LINEAR)
-
- def getTimeZone(self):
- """Sets tzinfo that is used if this axis plots date times.
-
- None means the datetimes are interpreted as local time.
-
- :rtype: datetime.tzinfo of None.
- """
- raise NotImplementedError()
-
- def setTimeZone(self, tz):
- """Sets tzinfo that is used if this axis' tickMode is TIME_SERIES
-
- The tz must be a descendant of the datetime.tzinfo class, "UTC" or None.
- Use None to let the datetimes be interpreted as local time.
- Use the string "UTC" to let the date datetimes be in UTC time.
-
- :param tz: datetime.tzinfo, "UTC" or None.
- """
- raise NotImplementedError()
-
- def getTickMode(self):
- """Determines if axis ticks are number or datetimes.
-
- :rtype: TickMode enum.
- """
- raise NotImplementedError()
-
- def setTickMode(self, tickMode):
- """Determines if axis ticks are number or datetimes.
-
- :param TickMode tickMode: tick mode enum.
- """
- raise NotImplementedError()
-
- def isAutoScale(self):
- """Return True if axis is automatically adjusting its limits.
-
- :rtype: bool
- """
- return self._isAutoScale
-
- def setAutoScale(self, flag=True):
- """Set the axis limits adjusting behavior of :meth:`resetZoom`.
-
- :param bool flag: True to resize limits automatically,
- False to disable it.
- """
- self._isAutoScale = bool(flag)
- self.sigAutoScaleChanged.emit(self._isAutoScale)
-
- def _setLimitsConstraints(self, minPos=None, maxPos=None):
- raise NotImplementedError()
-
- def setLimitsConstraints(self, minPos=None, maxPos=None):
- """
- Set a constraint on the position of the axes.
-
- :param float minPos: Minimum allowed axis value.
- :param float maxPos: Maximum allowed axis value.
- :return: True if the constaints was updated
- :rtype: bool
- """
- updated = self._setLimitsConstraints(minPos, maxPos)
- if updated:
- plot = self._getPlot()
- xMin, xMax = plot.getXAxis().getLimits()
- yMin, yMax = plot.getYAxis().getLimits()
- y2Min, y2Max = plot.getYAxis('right').getLimits()
- plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
- return updated
-
- def _setRangeConstraints(self, minRange=None, maxRange=None):
- raise NotImplementedError()
-
- def setRangeConstraints(self, minRange=None, maxRange=None):
- """
- Set a constraint on the position of the axes.
-
- :param float minRange: Minimum allowed left-to-right span across the
- view
- :param float maxRange: Maximum allowed left-to-right span across the
- view
- :return: True if the constaints was updated
- :rtype: bool
- """
- updated = self._setRangeConstraints(minRange, maxRange)
- if updated:
- plot = self._getPlot()
- xMin, xMax = plot.getXAxis().getLimits()
- yMin, yMax = plot.getYAxis().getLimits()
- y2Min, y2Max = plot.getYAxis('right').getLimits()
- plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
- return updated
-
-
-class XAxis(Axis):
- """Axis class defining primitives for the X axis"""
-
- # TODO With some changes on the backend, it will be able to remove all this
- # specialised implementations (prefixel by '_internal')
-
- def getTimeZone(self):
- return self._getBackend().getXAxisTimeZone()
-
- def setTimeZone(self, tz):
- if isinstance(tz, str) and tz.upper() == "UTC":
- tz = dateutil.tz.tzutc()
- elif not(tz is None or isinstance(tz, dt.tzinfo)):
- raise TypeError("tz must be a dt.tzinfo object, None or 'UTC'.")
-
- self._getBackend().setXAxisTimeZone(tz)
- self._getPlot()._setDirtyPlot()
-
- def getTickMode(self):
- if self._getBackend().isXAxisTimeSeries():
- return TickMode.TIME_SERIES
- else:
- return TickMode.DEFAULT
-
- def setTickMode(self, tickMode):
- if tickMode == TickMode.DEFAULT:
- self._getBackend().setXAxisTimeSeries(False)
- elif tickMode == TickMode.TIME_SERIES:
- self._getBackend().setXAxisTimeSeries(True)
- else:
- raise ValueError("Unexpected TickMode: {}".format(tickMode))
-
- def _internalSetCurrentLabel(self, label):
- self._getBackend().setGraphXLabel(label)
-
- def _internalGetLimits(self):
- return self._getBackend().getGraphXLimits()
-
- def _internalSetLimits(self, xmin, xmax):
- self._getBackend().setGraphXLimits(xmin, xmax)
-
- def _internalSetLogarithmic(self, flag):
- self._getBackend().setXAxisLogarithmic(flag)
-
- def _setLimitsConstraints(self, minPos=None, maxPos=None):
- constrains = self._getPlot()._getViewConstraints()
- updated = constrains.update(xMin=minPos, xMax=maxPos)
- return updated
-
- def _setRangeConstraints(self, minRange=None, maxRange=None):
- constrains = self._getPlot()._getViewConstraints()
- updated = constrains.update(minXRange=minRange, maxXRange=maxRange)
- return updated
-
-
-class YAxis(Axis):
- """Axis class defining primitives for the Y axis"""
-
- # TODO With some changes on the backend, it will be able to remove all this
- # specialised implementations (prefixel by '_internal')
-
- def _internalSetCurrentLabel(self, label):
- self._getBackend().setGraphYLabel(label, axis='left')
-
- def _internalGetLimits(self):
- return self._getBackend().getGraphYLimits(axis='left')
-
- def _internalSetLimits(self, ymin, ymax):
- self._getBackend().setGraphYLimits(ymin, ymax, axis='left')
-
- def _internalSetLogarithmic(self, flag):
- self._getBackend().setYAxisLogarithmic(flag)
-
- def setInverted(self, flag=True):
- """Set the axis orientation.
-
- This is only available for the Y axis.
-
- :param bool flag: True for Y axis going from top to bottom,
- False for Y axis going from bottom to top
- """
- flag = bool(flag)
- if self.isInverted() == flag:
- return
- self._getBackend().setYAxisInverted(flag)
- self._getPlot()._setDirtyPlot()
- self.sigInvertedChanged.emit(flag)
-
- def isInverted(self):
- """Return True if the axis is inverted (top to bottom for the y-axis),
- False otherwise. It is always False for the X axis.
-
- :rtype: bool
- """
- return self._getBackend().isYAxisInverted()
-
- def _setLimitsConstraints(self, minPos=None, maxPos=None):
- constrains = self._getPlot()._getViewConstraints()
- updated = constrains.update(yMin=minPos, yMax=maxPos)
- return updated
-
- def _setRangeConstraints(self, minRange=None, maxRange=None):
- constrains = self._getPlot()._getViewConstraints()
- updated = constrains.update(minYRange=minRange, maxYRange=maxRange)
- return updated
-
-
-class YRightAxis(Axis):
- """Proxy axis for the secondary Y axes. It manages it own label and limit
- but share the some state like scale and direction with the main axis."""
-
- # TODO With some changes on the backend, it will be able to remove all this
- # specialised implementations (prefixel by '_internal')
-
- def __init__(self, plot, mainAxis):
- """Constructor
-
- :param silx.gui.plot.PlotWidget.PlotWidget plot: Parent plot of this
- axis
- :param Axis mainAxis: Axis which sharing state with this axis
- """
- Axis.__init__(self, plot)
- self.__mainAxis = mainAxis
-
- @property
- def sigInvertedChanged(self):
- """Signal emitted when axis orientation has changed"""
- return self.__mainAxis.sigInvertedChanged
-
- @property
- def sigScaleChanged(self):
- """Signal emitted when axis scale has changed"""
- return self.__mainAxis.sigScaleChanged
-
- @property
- def _sigLogarithmicChanged(self):
- """Signal emitted when axis scale has changed to or from logarithmic"""
- return self.__mainAxis._sigLogarithmicChanged
-
- @property
- def sigAutoScaleChanged(self):
- """Signal emitted when axis autoscale has changed"""
- return self.__mainAxis.sigAutoScaleChanged
-
- def _internalSetCurrentLabel(self, label):
- self._getBackend().setGraphYLabel(label, axis='right')
-
- def _internalGetLimits(self):
- return self._getBackend().getGraphYLimits(axis='right')
-
- def _internalSetLimits(self, ymin, ymax):
- self._getBackend().setGraphYLimits(ymin, ymax, axis='right')
-
- def setInverted(self, flag=True):
- """Set the Y axis orientation.
-
- :param bool flag: True for Y axis going from top to bottom,
- False for Y axis going from bottom to top
- """
- return self.__mainAxis.setInverted(flag)
-
- def isInverted(self):
- """Return True if Y axis goes from top to bottom, False otherwise."""
- return self.__mainAxis.isInverted()
-
- def getScale(self):
- """Return the name of the scale used by this axis.
-
- :rtype: str
- """
- return self.__mainAxis.getScale()
-
- def setScale(self, scale):
- """Set the scale to be used by this axis.
-
- :param str scale: Name of the scale ("log", or "linear")
- """
- self.__mainAxis.setScale(scale)
-
- def _isLogarithmic(self):
- """Return True if Y axis scale is logarithmic, False if linear."""
- return self.__mainAxis._isLogarithmic()
-
- def _setLogarithmic(self, flag):
- """Set the Y axes scale (either linear or logarithmic).
-
- :param bool flag: True to use a logarithmic scale, False for linear.
- """
- return self.__mainAxis._setLogarithmic(flag)
-
- def isAutoScale(self):
- """Return True if Y axes are automatically adjusting its limits."""
- return self.__mainAxis.isAutoScale()
-
- def setAutoScale(self, flag=True):
- """Set the Y axis limits adjusting behavior of :meth:`PlotWidget.resetZoom`.
-
- :param bool flag: True to resize limits automatically,
- False to disable it.
- """
- return self.__mainAxis.setAutoScale(flag)
diff --git a/silx/gui/plot/items/complex.py b/silx/gui/plot/items/complex.py
deleted file mode 100644
index abb64ad..0000000
--- a/silx/gui/plot/items/complex.py
+++ /dev/null
@@ -1,386 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""This module provides the :class:`ImageComplexData` of the :class:`Plot`.
-"""
-
-from __future__ import absolute_import
-
-__authors__ = ["Vincent Favre-Nicolin", "T. Vincent"]
-__license__ = "MIT"
-__date__ = "14/06/2018"
-
-
-import logging
-
-import numpy
-
-from ....utils.proxy import docstring
-from ....utils.deprecation import deprecated
-from ...colors import Colormap
-from .core import ColormapMixIn, ComplexMixIn, ItemChangedType
-from .image import ImageBase
-
-
-_logger = logging.getLogger(__name__)
-
-
-# Complex colormap functions
-
-def _phase2rgb(colormap, data):
- """Creates RGBA image with colour-coded phase.
-
- :param Colormap colormap: The colormap to use
- :param numpy.ndarray data: The data to convert
- :return: Array of RGBA colors
- :rtype: numpy.ndarray
- """
- if data.size == 0:
- return numpy.zeros((0, 0, 4), dtype=numpy.uint8)
-
- phase = numpy.angle(data)
- return colormap.applyToData(phase)
-
-
-def _complex2rgbalog(phaseColormap, data, amin=0., dlogs=2, smax=None):
- """Returns RGBA colors: colour-coded phases and log10(amplitude) in alpha.
-
- :param Colormap phaseColormap: Colormap to use for the phase
- :param numpy.ndarray data: the complex data array to convert to RGBA
- :param float amin: the minimum value for the alpha channel
- :param float dlogs: amplitude range displayed, in log10 units
- :param float smax:
- if specified, all values above max will be displayed with an alpha=1
- """
- if data.size == 0:
- return numpy.zeros((0, 0, 4), dtype=numpy.uint8)
-
- rgba = _phase2rgb(phaseColormap, data)
- sabs = numpy.absolute(data)
- if smax is not None:
- sabs[sabs > smax] = smax
- a = numpy.log10(sabs + 1e-20)
- a -= a.max() - dlogs # display dlogs orders of magnitude
- rgba[..., 3] = 255 * (amin + a / dlogs * (1 - amin) * (a > 0))
- return rgba
-
-
-def _complex2rgbalin(phaseColormap, data, gamma=1.0, smax=None):
- """Returns RGBA colors: colour-coded phase and linear amplitude in alpha.
-
- :param Colormap phaseColormap: Colormap to use for the phase
- :param numpy.ndarray data:
- :param float gamma: Optional exponent gamma applied to the amplitude
- :param float smax:
- """
- if data.size == 0:
- return numpy.zeros((0, 0, 4), dtype=numpy.uint8)
-
- rgba = _phase2rgb(phaseColormap, data)
- a = numpy.absolute(data)
- if smax is not None:
- a[a > smax] = smax
- a /= a.max()
- rgba[..., 3] = 255 * a**gamma
- return rgba
-
-
-class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
- """Specific plot item to force colormap when using complex colormap.
-
- This is returning the specific colormap when displaying
- colored phase + amplitude.
- """
-
- _SUPPORTED_COMPLEX_MODES = (
- ComplexMixIn.ComplexMode.ABSOLUTE,
- ComplexMixIn.ComplexMode.PHASE,
- ComplexMixIn.ComplexMode.REAL,
- ComplexMixIn.ComplexMode.IMAGINARY,
- ComplexMixIn.ComplexMode.AMPLITUDE_PHASE,
- ComplexMixIn.ComplexMode.LOG10_AMPLITUDE_PHASE,
- ComplexMixIn.ComplexMode.SQUARE_AMPLITUDE)
- """Overrides supported ComplexMode"""
-
- def __init__(self):
- ImageBase.__init__(self, numpy.zeros((0, 0), dtype=numpy.complex64))
- ColormapMixIn.__init__(self)
- ComplexMixIn.__init__(self)
- self._dataByModesCache = {}
- self._amplitudeRangeInfo = None, 2
-
- # Use default from ColormapMixIn
- colormap = super(ImageComplexData, self).getColormap()
-
- phaseColormap = Colormap(
- name='hsv',
- vmin=-numpy.pi,
- vmax=numpy.pi)
-
- self._colormaps = { # Default colormaps for all modes
- self.ComplexMode.ABSOLUTE: colormap,
- self.ComplexMode.PHASE: phaseColormap,
- self.ComplexMode.REAL: colormap,
- self.ComplexMode.IMAGINARY: colormap,
- self.ComplexMode.AMPLITUDE_PHASE: phaseColormap,
- self.ComplexMode.LOG10_AMPLITUDE_PHASE: phaseColormap,
- self.ComplexMode.SQUARE_AMPLITUDE: colormap,
- }
-
- def _addBackendRenderer(self, backend):
- """Update backend renderer"""
- plot = self.getPlot()
- assert plot is not None
- if not self._isPlotLinear(plot):
- # Do not render with non linear scales
- return None
-
- mode = self.getComplexMode()
- if mode in (self.ComplexMode.AMPLITUDE_PHASE,
- self.ComplexMode.LOG10_AMPLITUDE_PHASE):
- # For those modes, compute RGBA image here
- colormap = None
- data = self.getRgbaImageData(copy=False)
- else:
- colormap = self.getColormap()
- if colormap.isAutoscale():
- # Avoid backend to compute autoscale: use item cache
- colormap = colormap.copy()
- colormap.setVRange(*colormap.getColormapRange(self))
-
- data = self.getData(copy=False)
-
- if data.size == 0:
- return None # No data to display
-
- return backend.addImage(data,
- origin=self.getOrigin(),
- scale=self.getScale(),
- colormap=colormap,
- alpha=self.getAlpha())
-
- @docstring(ComplexMixIn)
- def setComplexMode(self, mode):
- changed = super(ImageComplexData, self).setComplexMode(mode)
- if changed:
- self._valueDataChanged()
-
- # Backward compatibility
- self._updated(ItemChangedType.VISUALIZATION_MODE)
-
- # Update ColormapMixIn colormap
- colormap = self._colormaps[self.getComplexMode()]
- if colormap is not super(ImageComplexData, self).getColormap():
- super(ImageComplexData, self).setColormap(colormap)
-
- # Send data updated as value returned by getData has changed
- self._updated(ItemChangedType.DATA)
- return changed
-
- def _setAmplitudeRangeInfo(self, max_=None, delta=2):
- """Set the amplitude range to display for 'log10_amplitude_phase' mode.
-
- :param max_: Max of the amplitude range.
- If None it autoscales to data max.
- :param float delta: Delta range in log10 to display
- """
- self._amplitudeRangeInfo = max_, float(delta)
- self._updated(ItemChangedType.VISUALIZATION_MODE)
-
- def _getAmplitudeRangeInfo(self):
- """Returns the amplitude range to use for 'log10_amplitude_phase' mode.
-
- :return: (max, delta), if max is None, then it autoscales to data max
- :rtype: 2-tuple"""
- return self._amplitudeRangeInfo
-
- def setColormap(self, colormap, mode=None):
- """Set the colormap for this specific mode.
-
- :param ~silx.gui.colors.Colormap colormap: The colormap
- :param Union[ComplexMode,str] mode:
- If specified, set the colormap of this specific mode.
- Default: current mode.
- """
- if mode is None:
- mode = self.getComplexMode()
- else:
- mode = self.ComplexMode.from_value(mode)
-
- self._colormaps[mode] = colormap
- if mode is self.getComplexMode():
- super(ImageComplexData, self).setColormap(colormap)
- else:
- self._updated(ItemChangedType.COLORMAP)
-
- def getColormap(self, mode=None):
- """Get the colormap for the (current) mode.
-
- :param Union[ComplexMode,str] mode:
- If specified, get the colormap of this specific mode.
- Default: current mode.
- :rtype: ~silx.gui.colors.Colormap
- """
- if mode is None:
- mode = self.getComplexMode()
- else:
- mode = self.ComplexMode.from_value(mode)
-
- return self._colormaps[mode]
-
- def setData(self, data, copy=True):
- """"Set the image complex data
-
- :param numpy.ndarray data: 2D array of complex with 2 dimensions (h, w)
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- """
- data = numpy.array(data, copy=copy)
- assert data.ndim == 2
- if not numpy.issubdtype(data.dtype, numpy.complexfloating):
- _logger.warning(
- 'Image is not complex, converting it to complex to plot it.')
- data = numpy.array(data, dtype=numpy.complex64)
-
- # Compute current mode data and set colormap data
- mode = self.getComplexMode()
- dataForMode = self.__convertComplexData(data, self.getComplexMode())
- self._dataByModesCache = {mode: dataForMode}
-
- super().setData(data)
-
- def _updated(self, event=None, checkVisibility=True):
- # Synchronizes colormapped data if changed
- # ItemChangedType.COMPLEX_MODE triggers ItemChangedType.DATA
- # No need to handle it twice.
- if event in (ItemChangedType.DATA, ItemChangedType.MASK):
- # Color-mapped data is NOT the `getValueData` for some modes
- if self.getComplexMode() in (
- self.ComplexMode.AMPLITUDE_PHASE,
- self.ComplexMode.LOG10_AMPLITUDE_PHASE):
- data = self.getData(copy=False, mode=self.ComplexMode.PHASE)
- mask = self.getMaskData(copy=False)
- if mask is not None:
- data = numpy.copy(data)
- data[mask != 0] = numpy.nan
- else:
- data = self.getValueData(copy=False)
- self._setColormappedData(data, copy=False)
- super()._updated(event=event, checkVisibility=checkVisibility)
-
- def getComplexData(self, copy=True):
- """Returns the image complex data
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: numpy.ndarray of complex
- """
- return super().getData(copy=copy)
-
- def __convertComplexData(self, data, mode):
- """Convert complex data to given mode.
-
- :param numpy.ndarray data:
- :param Union[ComplexMode,str] mode:
- :rtype: numpy.ndarray of float
- """
- if mode is self.ComplexMode.PHASE:
- return numpy.angle(data)
- elif mode is self.ComplexMode.REAL:
- return numpy.real(data)
- elif mode is self.ComplexMode.IMAGINARY:
- return numpy.imag(data)
- elif mode in (self.ComplexMode.ABSOLUTE,
- self.ComplexMode.LOG10_AMPLITUDE_PHASE,
- self.ComplexMode.AMPLITUDE_PHASE):
- return numpy.absolute(data)
- elif mode is self.ComplexMode.SQUARE_AMPLITUDE:
- return numpy.absolute(data) ** 2
- else:
- _logger.error(
- 'Unsupported conversion mode: %s, fallback to absolute',
- str(mode))
- return numpy.absolute(data)
-
- def getData(self, copy=True, mode=None):
- """Returns the image data corresponding to (current) mode.
-
- The returned data is always floats, to get the complex data, use
- :meth:`getComplexData`.
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :param Union[ComplexMode,str] mode:
- If specified, get data corresponding to the mode.
- Default: Current mode.
- :rtype: numpy.ndarray of float
- """
- if mode is None:
- mode = self.getComplexMode()
- else:
- mode = self.ComplexMode.from_value(mode)
-
- if mode not in self._dataByModesCache:
- self._dataByModesCache[mode] = self.__convertComplexData(
- self.getComplexData(copy=False), mode)
-
- return numpy.array(self._dataByModesCache[mode], copy=copy)
-
- def getRgbaImageData(self, copy=True, mode=None):
- """Get the displayed RGB(A) image for (current) mode
-
- :param bool copy: Ignored for this class
- :param Union[ComplexMode,str] mode:
- If specified, get data corresponding to the mode.
- Default: Current mode.
- :rtype: numpy.ndarray of uint8 of shape (height, width, 4)
- """
- if mode is None:
- mode = self.getComplexMode()
- else:
- mode = self.ComplexMode.from_value(mode)
-
- colormap = self.getColormap(mode=mode)
- if mode is self.ComplexMode.AMPLITUDE_PHASE:
- data = self.getComplexData(copy=False)
- return _complex2rgbalin(colormap, data)
- elif mode is self.ComplexMode.LOG10_AMPLITUDE_PHASE:
- data = self.getComplexData(copy=False)
- max_, delta = self._getAmplitudeRangeInfo()
- return _complex2rgbalog(colormap, data, dlogs=delta, smax=max_)
- else:
- data = self.getData(copy=False, mode=mode)
- return colormap.applyToData(data)
-
- # Backward compatibility
-
- Mode = ComplexMixIn.ComplexMode
-
- @deprecated(replacement='setComplexMode', since_version='0.11.0')
- def setVisualizationMode(self, mode):
- return self.setComplexMode(mode)
-
- @deprecated(replacement='getComplexMode', since_version='0.11.0')
- def getVisualizationMode(self):
- return self.getComplexMode()
diff --git a/silx/gui/plot/items/core.py b/silx/gui/plot/items/core.py
deleted file mode 100644
index 95a65ad..0000000
--- a/silx/gui/plot/items/core.py
+++ /dev/null
@@ -1,1734 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""This module provides the base class for items of the :class:`Plot`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "08/12/2020"
-
-import collections
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
-from copy import deepcopy
-import logging
-import enum
-from typing import Optional, Tuple
-import warnings
-import weakref
-
-import numpy
-import six
-
-from ....utils.deprecation import deprecated
-from ....utils.proxy import docstring
-from ....utils.enum import Enum as _Enum
-from ....math.combo import min_max
-from ... import qt
-from ... import colors
-from ...colors import Colormap
-from ._pick import PickingResult
-
-from silx import config
-
-_logger = logging.getLogger(__name__)
-
-
-@enum.unique
-class ItemChangedType(enum.Enum):
- """Type of modification provided by :attr:`Item.sigItemChanged` signal."""
- # Private setters and setInfo are not emitting sigItemChanged signal.
- # Signals to consider:
- # COLORMAP_SET emitted when setColormap is called but not forward colormap object signal
- # CURRENT_COLOR_CHANGED emitted current color changed because highlight changed,
- # highlighted color changed or color changed depending on hightlight state.
-
- VISIBLE = 'visibleChanged'
- """Item's visibility changed flag."""
-
- ZVALUE = 'zValueChanged'
- """Item's Z value changed flag."""
-
- COLORMAP = 'colormapChanged' # Emitted when set + forward events from the colormap object
- """Item's colormap changed flag.
-
- This is emitted both when setting a new colormap and
- when the current colormap object is updated.
- """
-
- SYMBOL = 'symbolChanged'
- """Item's symbol changed flag."""
-
- SYMBOL_SIZE = 'symbolSizeChanged'
- """Item's symbol size changed flag."""
-
- LINE_WIDTH = 'lineWidthChanged'
- """Item's line width changed flag."""
-
- LINE_STYLE = 'lineStyleChanged'
- """Item's line style changed flag."""
-
- COLOR = 'colorChanged'
- """Item's color changed flag."""
-
- LINE_BG_COLOR = 'lineBgColorChanged'
- """Item's line background color changed flag."""
-
- YAXIS = 'yAxisChanged'
- """Item's Y axis binding changed flag."""
-
- FILL = 'fillChanged'
- """Item's fill changed flag."""
-
- ALPHA = 'alphaChanged'
- """Item's transparency alpha changed flag."""
-
- DATA = 'dataChanged'
- """Item's data changed flag"""
-
- MASK = 'maskChanged'
- """Item's mask changed flag"""
-
- HIGHLIGHTED = 'highlightedChanged'
- """Item's highlight state changed flag."""
-
- HIGHLIGHTED_COLOR = 'highlightedColorChanged'
- """Deprecated, use HIGHLIGHTED_STYLE instead."""
-
- HIGHLIGHTED_STYLE = 'highlightedStyleChanged'
- """Item's highlighted style changed flag."""
-
- SCALE = 'scaleChanged'
- """Item's scale changed flag."""
-
- TEXT = 'textChanged'
- """Item's text changed flag."""
-
- POSITION = 'positionChanged'
- """Item's position changed flag.
-
- This is emitted when a marker position changed and
- when an image origin changed.
- """
-
- OVERLAY = 'overlayChanged'
- """Item's overlay state changed flag."""
-
- VISUALIZATION_MODE = 'visualizationModeChanged'
- """Item's visualization mode changed flag."""
-
- COMPLEX_MODE = 'complexModeChanged'
- """Item's complex data visualization mode changed flag."""
-
- NAME = 'nameChanged'
- """Item's name changed flag."""
-
- EDITABLE = 'editableChanged'
- """Item's editable state changed flags."""
-
- SELECTABLE = 'selectableChanged'
- """Item's selectable state changed flags."""
-
-
-class Item(qt.QObject):
- """Description of an item of the plot"""
-
- _DEFAULT_Z_LAYER = 0
- """Default layer for overlay rendering"""
-
- _DEFAULT_SELECTABLE = False
- """Default selectable state of items"""
-
- sigItemChanged = qt.Signal(object)
- """Signal emitted when the item has changed.
-
- It provides a flag describing which property of the item has changed.
- See :class:`ItemChangedType` for flags description.
- """
-
- _sigVisibleBoundsChanged = qt.Signal()
- """Signal emitted when the visible extent of the item in the plot has changed.
-
- This signal is emitted only if visible extent tracking is enabled
- (see :meth:`_setVisibleBoundsTracking`).
- """
-
- def __init__(self):
- qt.QObject.__init__(self)
- self._dirty = True
- self._plotRef = None
- self._visible = True
- self._selectable = self._DEFAULT_SELECTABLE
- self._z = self._DEFAULT_Z_LAYER
- self._info = None
- self._xlabel = None
- self._ylabel = None
- self.__name = ''
-
- self.__visibleBoundsTracking = False
- self.__previousVisibleBounds = None
-
- self._backendRenderer = None
-
- def getPlot(self):
- """Returns the ~silx.gui.plot.PlotWidget this item belongs to.
-
- :rtype: Union[~silx.gui.plot.PlotWidget,None]
- """
- return None if self._plotRef is None else self._plotRef()
-
- def _setPlot(self, plot):
- """Set the plot this item belongs to.
-
- WARNING: This should only be called from the Plot.
-
- :param Union[~silx.gui.plot.PlotWidget,None] plot: The Plot instance.
- """
- if plot is not None and self._plotRef is not None:
- raise RuntimeError('Trying to add a node at two places.')
- self.__disconnectFromPlotWidget()
- self._plotRef = None if plot is None else weakref.ref(plot)
- self.__connectToPlotWidget()
- self._updated()
-
- def getBounds(self): # TODO return a Bounds object rather than a tuple
- """Returns the bounding box of this item in data coordinates
-
- :returns: (xmin, xmax, ymin, ymax) or None
- :rtype: 4-tuple of float or None
- """
- return self._getBounds()
-
- def _getBounds(self):
- """:meth:`getBounds` implementation to override by sub-class"""
- return None
-
- def isVisible(self):
- """True if item is visible, False otherwise
-
- :rtype: bool
- """
- return self._visible
-
- def setVisible(self, visible):
- """Set visibility of item.
-
- :param bool visible: True to display it, False otherwise
- """
- visible = bool(visible)
- if visible != self._visible:
- self._visible = visible
- # When visibility has changed, always mark as dirty
- self._updated(ItemChangedType.VISIBLE,
- checkVisibility=False)
-
- def isOverlay(self):
- """Return true if item is drawn as an overlay.
-
- :rtype: bool
- """
- return False
-
- def getName(self):
- """Returns the name of the item which is used as legend.
-
- :rtype: str
- """
- return self.__name
-
- def setName(self, name):
- """Set the name of the item which is used as legend.
-
- :param str name: New name of the item
- :raises RuntimeError: If item belongs to a PlotWidget.
- """
- name = str(name)
- if self.__name != name:
- if self.getPlot() is not None:
- raise RuntimeError(
- "Cannot change name while item is in a PlotWidget")
-
- self.__name = name
- self._updated(ItemChangedType.NAME)
-
- def getLegend(self): # Replaced by getName for API consistency
- return self.getName()
-
- @deprecated(replacement='setName', since_version='0.13')
- def _setLegend(self, legend):
- legend = str(legend) if legend is not None else ''
- self.setName(legend)
-
- def isSelectable(self):
- """Returns true if item is selectable (bool)"""
- return self._selectable
-
- def _setSelectable(self, selectable): # TODO support update
- """Set whether item is selectable or not.
-
- This is private for now as change is not handled.
-
- :param bool selectable: True to make item selectable
- """
- self._selectable = bool(selectable)
-
- def getZValue(self):
- """Returns the layer on which to draw this item (int)"""
- return self._z
-
- def setZValue(self, z):
- z = int(z) if z is not None else self._DEFAULT_Z_LAYER
- if z != self._z:
- self._z = z
- self._updated(ItemChangedType.ZVALUE)
-
- def getInfo(self, copy=True):
- """Returns the info associated to this item
-
- :param bool copy: True to get a deepcopy, False otherwise.
- """
- return deepcopy(self._info) if copy else self._info
-
- def setInfo(self, info, copy=True):
- if copy:
- info = deepcopy(info)
- self._info = info
-
- def getVisibleBounds(self) -> Optional[Tuple[float, float, float, float]]:
- """Returns visible bounds of the item bounding box in the plot area.
-
- :returns:
- (xmin, xmax, ymin, ymax) in data coordinates of the visible area or
- None if item is not visible in the plot area.
- :rtype: Union[List[float],None]
- """
- plot = self.getPlot()
- bounds = self.getBounds()
- if plot is None or bounds is None or not self.isVisible():
- return None
-
- xmin, xmax = numpy.clip(bounds[:2], *plot.getXAxis().getLimits())
- ymin, ymax = numpy.clip(
- bounds[2:], *plot.getYAxis(self.__getYAxis()).getLimits())
-
- if xmin == xmax or ymin == ymax: # Outside the plot area
- return None
- else:
- return xmin, xmax, ymin, ymax
-
- def _isVisibleBoundsTracking(self) -> bool:
- """Returns True if visible bounds changes are tracked.
-
- When enabled, :attr:`_sigVisibleBoundsChanged` is emitted upon changes.
- :rtype: bool
- """
- return self.__visibleBoundsTracking
-
- def _setVisibleBoundsTracking(self, enable: bool) -> None:
- """Set whether or not to track visible bounds changes.
-
- :param bool enable:
- """
- if enable != self.__visibleBoundsTracking:
- self.__disconnectFromPlotWidget()
- self.__previousVisibleBounds = None
- self.__visibleBoundsTracking = enable
- self.__connectToPlotWidget()
-
- def __getYAxis(self) -> str:
- """Returns current Y axis ('left' or 'right')"""
- return self.getYAxis() if isinstance(self, YAxisMixIn) else 'left'
-
- def __connectToPlotWidget(self) -> None:
- """Connect to PlotWidget signals and install event filter"""
- if not self._isVisibleBoundsTracking():
- return
-
- plot = self.getPlot()
- if plot is not None:
- for axis in (plot.getXAxis(), plot.getYAxis(self.__getYAxis())):
- axis.sigLimitsChanged.connect(self._visibleBoundsChanged)
-
- plot.installEventFilter(self)
-
- self._visibleBoundsChanged()
-
- def __disconnectFromPlotWidget(self) -> None:
- """Disconnect from PlotWidget signals and remove event filter"""
- if not self._isVisibleBoundsTracking():
- return
-
- plot = self.getPlot()
- if plot is not None:
- for axis in (plot.getXAxis(), plot.getYAxis(self.__getYAxis())):
- axis.sigLimitsChanged.disconnect(self._visibleBoundsChanged)
-
- plot.removeEventFilter(self)
-
- def _visibleBoundsChanged(self, *args) -> None:
- """Check if visible extent actually changed and emit signal"""
- if not self._isVisibleBoundsTracking():
- return # No visible extent tracking
-
- plot = self.getPlot()
- if plot is None or not plot.isVisible():
- return # No plot or plot not visible
-
- extent = self.getVisibleBounds()
- if extent != self.__previousVisibleBounds:
- self.__previousVisibleBounds = extent
- self._sigVisibleBoundsChanged.emit()
-
- def eventFilter(self, watched, event):
- """Event filter to handle PlotWidget show events"""
- if watched is self.getPlot() and event.type() == qt.QEvent.Show:
- self._visibleBoundsChanged()
- return super().eventFilter(watched, event)
-
- def _updated(self, event=None, checkVisibility=True):
- """Mark the item as dirty (i.e., needing update).
-
- This also triggers Plot.replot.
-
- :param event: The event to send to :attr:`sigItemChanged` signal.
- :param bool checkVisibility: True to only mark as dirty if visible,
- False to always mark as dirty.
- """
- if not checkVisibility or self.isVisible():
- if not self._dirty:
- self._dirty = True
- # TODO: send event instead of explicit call
- plot = self.getPlot()
- if plot is not None:
- plot._itemRequiresUpdate(self)
- if event is not None:
- self.sigItemChanged.emit(event)
-
- def _update(self, backend):
- """Called by Plot to update the backend for this item.
-
- This is meant to be called asynchronously from _updated.
- This optimizes the number of call to _update.
-
- :param backend: The backend to update
- """
- if self._dirty:
- # Remove previous renderer from backend if any
- self._removeBackendRenderer(backend)
-
- # If not visible, do not add renderer to backend
- if self.isVisible():
- self._backendRenderer = self._addBackendRenderer(backend)
-
- self._dirty = False
-
- def _addBackendRenderer(self, backend):
- """Override in subclass to add specific backend renderer.
-
- :param BackendBase backend: The backend to update
- :return: The renderer handle to store or None if no renderer in backend
- """
- return None
-
- def _removeBackendRenderer(self, backend):
- """Override in subclass to remove specific backend renderer.
-
- :param BackendBase backend: The backend to update
- """
- if self._backendRenderer is not None:
- backend.remove(self._backendRenderer)
- self._backendRenderer = None
-
- def pick(self, x, y):
- """Run picking test on this item
-
- :param float x: The x pixel coord where to pick.
- :param float y: The y pixel coord where to pick.
- :return: None if not picked, else the picked position information
- :rtype: Union[None,PickingResult]
- """
- if not self.isVisible() or self._backendRenderer is None:
- return None
- plot = self.getPlot()
- if plot is None:
- return None
-
- indices = plot._backend.pickItem(x, y, self._backendRenderer)
- if indices is None:
- return None
- else:
- return PickingResult(self, indices)
-
-
-class DataItem(Item):
- """Item with a data extent in the plot"""
-
- def _boundsChanged(self, checkVisibility: bool=True) -> None:
- """Call this method in subclass when data bounds has changed.
-
- :param bool checkVisibility:
- """
- if not checkVisibility or self.isVisible():
- self._visibleBoundsChanged()
-
- # TODO hackish data range implementation
- plot = self.getPlot()
- if plot is not None:
- plot._invalidateDataRange()
-
- @docstring(Item)
- def setVisible(self, visible: bool):
- if visible != self.isVisible():
- self._boundsChanged(checkVisibility=False)
- super().setVisible(visible)
-
-# Mix-in classes ##############################################################
-
-
-class ItemMixInBase(object):
- """Base class for Item mix-in"""
-
- def _updated(self, event=None, checkVisibility=True):
- """This is implemented in :class:`Item`.
-
- Mark the item as dirty (i.e., needing update).
- This also triggers Plot.replot.
-
- :param event: The event to send to :attr:`sigItemChanged` signal.
- :param bool checkVisibility: True to only mark as dirty if visible,
- False to always mark as dirty.
- """
- raise RuntimeError(
- "Issue with Mix-In class inheritance order")
-
-
-class LabelsMixIn(ItemMixInBase):
- """Mix-in class for items with x and y labels
-
- Setters are private, otherwise it needs to check the plot
- current active curve and access the internal current labels.
- """
-
- def __init__(self):
- self._xlabel = None
- self._ylabel = None
-
- def getXLabel(self):
- """Return the X axis label associated to this curve
-
- :rtype: str or None
- """
- return self._xlabel
-
- def _setXLabel(self, label):
- """Set the X axis label associated with this curve
-
- :param str label: The X axis label
- """
- self._xlabel = str(label)
-
- def getYLabel(self):
- """Return the Y axis label associated to this curve
-
- :rtype: str or None
- """
- return self._ylabel
-
- def _setYLabel(self, label):
- """Set the Y axis label associated with this curve
-
- :param str label: The Y axis label
- """
- self._ylabel = str(label)
-
-
-class DraggableMixIn(ItemMixInBase):
- """Mix-in class for draggable items"""
-
- def __init__(self):
- self._draggable = False
-
- def isDraggable(self):
- """Returns true if image is draggable
-
- :rtype: bool
- """
- return self._draggable
-
- def _setDraggable(self, draggable): # TODO support update
- """Set if image is draggable or not.
-
- This is private for not as it does not support update.
-
- :param bool draggable:
- """
- self._draggable = bool(draggable)
-
- def drag(self, from_, to):
- """Perform a drag of the item.
-
- :param List[float] from_: (x, y) previous position in data coordinates
- :param List[float] to: (x, y) current position in data coordinates
- """
- raise NotImplementedError("Must be implemented in subclass")
-
-
-class ColormapMixIn(ItemMixInBase):
- """Mix-in class for items with colormap"""
-
- def __init__(self):
- self._colormap = Colormap()
- self._colormap.sigChanged.connect(self._colormapChanged)
- self.__data = None
- self.__cacheColormapRange = {} # Store {normalization: range}
-
- def getColormap(self):
- """Return the used colormap"""
- return self._colormap
-
- def setColormap(self, colormap):
- """Set the colormap of this item
-
- :param silx.gui.colors.Colormap colormap: colormap description
- """
- if self._colormap is colormap:
- return
- if isinstance(colormap, dict):
- colormap = Colormap._fromDict(colormap)
-
- if self._colormap is not None:
- self._colormap.sigChanged.disconnect(self._colormapChanged)
- self._colormap = colormap
- if self._colormap is not None:
- self._colormap.sigChanged.connect(self._colormapChanged)
- self._colormapChanged()
-
- def _colormapChanged(self):
- """Handle updates of the colormap"""
- self._updated(ItemChangedType.COLORMAP)
-
- def _setColormappedData(self, data, copy=True,
- min_=None, minPositive=None, max_=None):
- """Set the data used to compute the colormapped display.
-
- It also resets the cache of data ranges.
-
- This method MUST be called by inheriting classes when data is updated.
-
- :param Union[None,numpy.ndarray] data:
- :param Union[None,float] min_: Minimum value of the data
- :param Union[None,float] minPositive:
- Minimum of strictly positive values of the data
- :param Union[None,float] max_: Maximum value of the data
- """
- self.__data = None if data is None else numpy.array(data, copy=copy)
- self.__cacheColormapRange = {} # Reset cache
-
- # Fill-up colormap range cache if values are provided
- if max_ is not None and numpy.isfinite(max_):
- if min_ is not None and numpy.isfinite(min_):
- self.__cacheColormapRange[Colormap.LINEAR, Colormap.MINMAX] = min_, max_
- if minPositive is not None and numpy.isfinite(minPositive):
- self.__cacheColormapRange[Colormap.LOGARITHM, Colormap.MINMAX] = minPositive, max_
-
- colormap = self.getColormap()
- if None in (colormap.getVMin(), colormap.getVMax()):
- self._colormapChanged()
-
- def getColormappedData(self, copy=True):
- """Returns the data used to compute the displayed colors
-
- :param bool copy: True to get a copy,
- False to get internal data (do not modify!).
- :rtype: Union[None,numpy.ndarray]
- """
- if self.__data is None:
- return None
- else:
- return numpy.array(self.__data, copy=copy)
-
- def _getColormapAutoscaleRange(self, colormap=None):
- """Returns the autoscale range for current data and colormap.
-
- :param Union[None,~silx.gui.colors.Colormap] colormap:
- The colormap for which to compute the autoscale range.
- If None, the default, the colormap of the item is used
- :return: (vmin, vmax) range (vmin and /or vmax might be `None`)
- """
- if colormap is None:
- colormap = self.getColormap()
-
- data = self.getColormappedData(copy=False)
- if colormap is None or data is None:
- return None, None
-
- normalization = colormap.getNormalization()
- autoscaleMode = colormap.getAutoscaleMode()
- key = normalization, autoscaleMode
- vRange = self.__cacheColormapRange.get(key, None)
- if vRange is None:
- vRange = colormap._computeAutoscaleRange(data)
- self.__cacheColormapRange[key] = vRange
- return vRange
-
-
-class SymbolMixIn(ItemMixInBase):
- """Mix-in class for items with symbol type"""
-
- _DEFAULT_SYMBOL = None
- """Default marker of the item"""
-
- _DEFAULT_SYMBOL_SIZE = config.DEFAULT_PLOT_SYMBOL_SIZE
- """Default marker size of the item"""
-
- _SUPPORTED_SYMBOLS = collections.OrderedDict((
- ('o', 'Circle'),
- ('d', 'Diamond'),
- ('s', 'Square'),
- ('+', 'Plus'),
- ('x', 'Cross'),
- ('.', 'Point'),
- (',', 'Pixel'),
- ('|', 'Vertical line'),
- ('_', 'Horizontal line'),
- ('tickleft', 'Tick left'),
- ('tickright', 'Tick right'),
- ('tickup', 'Tick up'),
- ('tickdown', 'Tick down'),
- ('caretleft', 'Caret left'),
- ('caretright', 'Caret right'),
- ('caretup', 'Caret up'),
- ('caretdown', 'Caret down'),
- (u'\u2665', 'Heart'),
- ('', 'None')))
- """Dict of supported symbols"""
-
- def __init__(self):
- if self._DEFAULT_SYMBOL is None: # Use default from config
- self._symbol = config.DEFAULT_PLOT_SYMBOL
- else:
- self._symbol = self._DEFAULT_SYMBOL
-
- if self._DEFAULT_SYMBOL_SIZE is None: # Use default from config
- self._symbol_size = config.DEFAULT_PLOT_SYMBOL_SIZE
- else:
- self._symbol_size = self._DEFAULT_SYMBOL_SIZE
-
- @classmethod
- def getSupportedSymbols(cls):
- """Returns the list of supported symbol names.
-
- :rtype: tuple of str
- """
- return tuple(cls._SUPPORTED_SYMBOLS.keys())
-
- @classmethod
- def getSupportedSymbolNames(cls):
- """Returns the list of supported symbol human-readable names.
-
- :rtype: tuple of str
- """
- return tuple(cls._SUPPORTED_SYMBOLS.values())
-
- def getSymbolName(self, symbol=None):
- """Returns human-readable name for a symbol.
-
- :param str symbol: The symbol from which to get the name.
- Default: current symbol.
- :rtype: str
- :raise KeyError: if symbol is not in :meth:`getSupportedSymbols`.
- """
- if symbol is None:
- symbol = self.getSymbol()
- return self._SUPPORTED_SYMBOLS[symbol]
-
- def getSymbol(self):
- """Return the point marker type.
-
- Marker type::
-
- - 'o' circle
- - '.' point
- - ',' pixel
- - '+' cross
- - 'x' x-cross
- - 'd' diamond
- - 's' square
-
- :rtype: str
- """
- return self._symbol
-
- def setSymbol(self, symbol):
- """Set the marker type
-
- See :meth:`getSymbol`.
-
- :param str symbol: Marker type or marker name
- """
- if symbol is None:
- symbol = self._DEFAULT_SYMBOL
-
- elif symbol not in self.getSupportedSymbols():
- for symbolCode, name in self._SUPPORTED_SYMBOLS.items():
- if name.lower() == symbol.lower():
- symbol = symbolCode
- break
- else:
- raise ValueError('Unsupported symbol %s' % str(symbol))
-
- if symbol != self._symbol:
- self._symbol = symbol
- self._updated(ItemChangedType.SYMBOL)
-
- def getSymbolSize(self):
- """Return the point marker size in points.
-
- :rtype: float
- """
- return self._symbol_size
-
- def setSymbolSize(self, size):
- """Set the point marker size in points.
-
- See :meth:`getSymbolSize`.
-
- :param str symbol: Marker type
- """
- if size is None:
- size = self._DEFAULT_SYMBOL_SIZE
- if size != self._symbol_size:
- self._symbol_size = size
- self._updated(ItemChangedType.SYMBOL_SIZE)
-
-
-class LineMixIn(ItemMixInBase):
- """Mix-in class for item with line"""
-
- _DEFAULT_LINEWIDTH = 1.
- """Default line width"""
-
- _DEFAULT_LINESTYLE = '-'
- """Default line style"""
-
- _SUPPORTED_LINESTYLE = '', ' ', '-', '--', '-.', ':', None
- """Supported line styles"""
-
- def __init__(self):
- self._linewidth = self._DEFAULT_LINEWIDTH
- self._linestyle = self._DEFAULT_LINESTYLE
-
- @classmethod
- def getSupportedLineStyles(cls):
- """Returns list of supported line styles.
-
- :rtype: List[str,None]
- """
- return cls._SUPPORTED_LINESTYLE
-
- def getLineWidth(self):
- """Return the curve line width in pixels
-
- :rtype: float
- """
- return self._linewidth
-
- def setLineWidth(self, width):
- """Set the width in pixel of the curve line
-
- See :meth:`getLineWidth`.
-
- :param float width: Width in pixels
- """
- width = float(width)
- if width != self._linewidth:
- self._linewidth = width
- self._updated(ItemChangedType.LINE_WIDTH)
-
- def getLineStyle(self):
- """Return the type of the line
-
- Type of line::
-
- - ' ' no line
- - '-' solid line
- - '--' dashed line
- - '-.' dash-dot line
- - ':' dotted line
-
- :rtype: str
- """
- return self._linestyle
-
- def setLineStyle(self, style):
- """Set the style of the curve line.
-
- See :meth:`getLineStyle`.
-
- :param str style: Line style
- """
- style = str(style)
- assert style in self.getSupportedLineStyles()
- if style is None:
- style = self._DEFAULT_LINESTYLE
- if style != self._linestyle:
- self._linestyle = style
- self._updated(ItemChangedType.LINE_STYLE)
-
-
-class ColorMixIn(ItemMixInBase):
- """Mix-in class for item with color"""
-
- _DEFAULT_COLOR = (0., 0., 0., 1.)
- """Default color of the item"""
-
- def __init__(self):
- self._color = self._DEFAULT_COLOR
-
- def getColor(self):
- """Returns the RGBA color of the item
-
- :rtype: 4-tuple of float in [0, 1] or array of colors
- """
- return self._color
-
- def setColor(self, color, copy=True):
- """Set item color
-
- :param color: color(s) to be used
- :type color: str ("#RRGGBB") or (npoints, 4) unsigned byte array or
- one of the predefined color names defined in colors.py
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- """
- if isinstance(color, six.string_types):
- color = colors.rgba(color)
- elif isinstance(color, qt.QColor):
- color = colors.rgba(color)
- else:
- color = numpy.array(color, copy=copy)
- # TODO more checks + improve color array support
- if color.ndim == 1: # Single RGBA color
- color = colors.rgba(color)
- else: # Array of colors
- assert color.ndim == 2
-
- self._color = color
- self._updated(ItemChangedType.COLOR)
-
-
-class YAxisMixIn(ItemMixInBase):
- """Mix-in class for item with yaxis"""
-
- _DEFAULT_YAXIS = 'left'
- """Default Y axis the item belongs to"""
-
- def __init__(self):
- self._yaxis = self._DEFAULT_YAXIS
-
- def getYAxis(self):
- """Returns the Y axis this curve belongs to.
-
- Either 'left' or 'right'.
-
- :rtype: str
- """
- return self._yaxis
-
- def setYAxis(self, yaxis):
- """Set the Y axis this curve belongs to.
-
- :param str yaxis: 'left' or 'right'
- """
- yaxis = str(yaxis)
- assert yaxis in ('left', 'right')
- if yaxis != self._yaxis:
- self._yaxis = yaxis
- # Handle data extent changed for DataItem
- if isinstance(self, DataItem):
- self._boundsChanged()
-
- # Handle visible extent changed
- if self._isVisibleBoundsTracking():
- # Switch Y axis signal connection
- plot = self.getPlot()
- if plot is not None:
- previousYAxis = 'left' if self.getXAxis() == 'right' else 'right'
- plot.getYAxis(previousYAxis).sigLimitsChanged.disconnect(
- self._visibleBoundsChanged)
- plot.getYAxis(self.getYAxis()).sigLimitsChanged.connect(
- self._visibleBoundsChanged)
- self._visibleBoundsChanged()
-
- self._updated(ItemChangedType.YAXIS)
-
-
-class FillMixIn(ItemMixInBase):
- """Mix-in class for item with fill"""
-
- def __init__(self):
- self._fill = False
-
- def isFill(self):
- """Returns whether the item is filled or not.
-
- :rtype: bool
- """
- return self._fill
-
- def setFill(self, fill):
- """Set whether to fill the item or not.
-
- :param bool fill:
- """
- fill = bool(fill)
- if fill != self._fill:
- self._fill = fill
- self._updated(ItemChangedType.FILL)
-
-
-class AlphaMixIn(ItemMixInBase):
- """Mix-in class for item with opacity"""
-
- def __init__(self):
- self._alpha = 1.
-
- def getAlpha(self):
- """Returns the opacity of the item
-
- :rtype: float in [0, 1.]
- """
- return self._alpha
-
- def setAlpha(self, alpha):
- """Set the opacity of the item
-
- .. note::
-
- If the colormap already has some transparency, this alpha
- adds additional transparency. The alpha channel of the colormap
- is multiplied by this value.
-
- :param alpha: Opacity of the item, between 0 (full transparency)
- and 1. (full opacity)
- :type alpha: float
- """
- alpha = float(alpha)
- alpha = max(0., min(alpha, 1.)) # Clip alpha to [0., 1.] range
- if alpha != self._alpha:
- self._alpha = alpha
- self._updated(ItemChangedType.ALPHA)
-
-
-class ComplexMixIn(ItemMixInBase):
- """Mix-in class for complex data mode"""
-
- _SUPPORTED_COMPLEX_MODES = None
- """Override to only support a subset of all ComplexMode"""
-
- class ComplexMode(_Enum):
- """Identify available display mode for complex"""
- NONE = 'none'
- ABSOLUTE = 'amplitude'
- PHASE = 'phase'
- REAL = 'real'
- IMAGINARY = 'imaginary'
- AMPLITUDE_PHASE = 'amplitude_phase'
- LOG10_AMPLITUDE_PHASE = 'log10_amplitude_phase'
- SQUARE_AMPLITUDE = 'square_amplitude'
-
- def __init__(self):
- self.__complex_mode = self.ComplexMode.ABSOLUTE
-
- def getComplexMode(self):
- """Returns the current complex visualization mode.
-
- :rtype: ComplexMode
- """
- return self.__complex_mode
-
- def setComplexMode(self, mode):
- """Set the complex visualization mode.
-
- :param ComplexMode mode: The visualization mode in:
- 'real', 'imaginary', 'phase', 'amplitude'
- :return: True if value was set, False if is was already set
- :rtype: bool
- """
- mode = self.ComplexMode.from_value(mode)
- assert mode in self.supportedComplexModes()
-
- if mode != self.__complex_mode:
- self.__complex_mode = mode
- self._updated(ItemChangedType.COMPLEX_MODE)
- return True
- else:
- return False
-
- def _convertComplexData(self, data, mode=None):
- """Convert complex data to the specific mode.
-
- :param Union[ComplexMode,None] mode:
- The kind of value to compute.
- If None (the default), the current complex mode is used.
- :return: The converted dataset
- :rtype: Union[numpy.ndarray[float],None]
- """
- if data is None:
- return None
-
- if mode is None:
- mode = self.getComplexMode()
-
- if mode is self.ComplexMode.REAL:
- return numpy.real(data)
- elif mode is self.ComplexMode.IMAGINARY:
- return numpy.imag(data)
- elif mode is self.ComplexMode.ABSOLUTE:
- return numpy.absolute(data)
- elif mode is self.ComplexMode.PHASE:
- return numpy.angle(data)
- elif mode is self.ComplexMode.SQUARE_AMPLITUDE:
- return numpy.absolute(data) ** 2
- else:
- raise ValueError('Unsupported conversion mode: %s', str(mode))
-
- @classmethod
- def supportedComplexModes(cls):
- """Returns the list of supported complex visualization modes.
-
- See :class:`ComplexMode` and :meth:`setComplexMode`.
-
- :rtype: List[ComplexMode]
- """
- if cls._SUPPORTED_COMPLEX_MODES is None:
- return cls.ComplexMode.members()
- else:
- return cls._SUPPORTED_COMPLEX_MODES
-
-
-class ScatterVisualizationMixIn(ItemMixInBase):
- """Mix-in class for scatter plot visualization modes"""
-
- _SUPPORTED_SCATTER_VISUALIZATION = None
- """Allows to override supported Visualizations"""
-
- @enum.unique
- class Visualization(_Enum):
- """Different modes of scatter plot visualizations"""
-
- POINTS = 'points'
- """Display scatter plot as a point cloud"""
-
- LINES = 'lines'
- """Display scatter plot as a wireframe.
-
- This is based on Delaunay triangulation
- """
-
- SOLID = 'solid'
- """Display scatter plot as a set of filled triangles.
-
- This is based on Delaunay triangulation
- """
-
- REGULAR_GRID = 'regular_grid'
- """Display scatter plot as an image.
-
- It expects the points to be the intersection of a regular grid,
- and the order of points following that of an image.
- First line, then second one, and always in the same direction
- (either all lines from left to right or all from right to left).
- """
-
- IRREGULAR_GRID = 'irregular_grid'
- """Display scatter plot as contiguous quadrilaterals.
-
- It expects the points to be the intersection of an irregular grid,
- and the order of points following that of an image.
- First line, then second one, and always in the same direction
- (either all lines from left to right or all from right to left).
- """
-
- BINNED_STATISTIC = 'binned_statistic'
- """Display scatter plot as 2D binned statistic (i.e., generalized histogram).
- """
-
- @enum.unique
- class VisualizationParameter(_Enum):
- """Different parameter names for scatter plot visualizations"""
-
- GRID_MAJOR_ORDER = 'grid_major_order'
- """The major order of points in the regular grid.
-
- Either 'row' (row-major, fast X) or 'column' (column-major, fast Y).
- """
-
- GRID_BOUNDS = 'grid_bounds'
- """The expected range in data coordinates of the regular grid.
-
- A 2-tuple of 2-tuple: (begin (x, y), end (x, y)).
- This provides the data coordinates of the first point and the expected
- last on.
- As for `GRID_SHAPE`, this can be wider than the current data.
- """
-
- GRID_SHAPE = 'grid_shape'
- """The expected size of the regular grid (height, width).
-
- The given shape can be wider than the number of points,
- in which case the grid is not fully filled.
- """
-
- BINNED_STATISTIC_SHAPE = 'binned_statistic_shape'
- """The number of bins in each dimension (height, width).
- """
-
- BINNED_STATISTIC_FUNCTION = 'binned_statistic_function'
- """The reduction function to apply to each bin (str).
-
- Available reduction functions are: 'mean' (default), 'count', 'sum'.
- """
-
- DATA_BOUNDS_HINT = 'data_bounds_hint'
- """The expected bounds of the data in data coordinates.
-
- A 2-tuple of 2-tuple: ((ymin, ymax), (xmin, xmax)).
- This provides a hint for the data ranges in both dimensions.
- It is eventually enlarged with actually data ranges.
-
- WARNING: dimension 0 i.e., Y first.
- """
-
- _SUPPORTED_VISUALIZATION_PARAMETER_VALUES = {
- VisualizationParameter.GRID_MAJOR_ORDER: ('row', 'column'),
- VisualizationParameter.BINNED_STATISTIC_FUNCTION: ('mean', 'count', 'sum'),
- }
- """Supported visualization parameter values.
-
- Defined for parameters with a set of acceptable values.
- """
-
- def __init__(self):
- self.__visualization = self.Visualization.POINTS
- self.__parameters = dict(# Init parameters to None
- (parameter, None) for parameter in self.VisualizationParameter)
- self.__parameters[self.VisualizationParameter.BINNED_STATISTIC_FUNCTION] = 'mean'
-
- @classmethod
- def supportedVisualizations(cls):
- """Returns the list of supported scatter visualization modes.
-
- See :meth:`setVisualization`
-
- :rtype: List[Visualization]
- """
- if cls._SUPPORTED_SCATTER_VISUALIZATION is None:
- return cls.Visualization.members()
- else:
- return cls._SUPPORTED_SCATTER_VISUALIZATION
-
- @classmethod
- def supportedVisualizationParameterValues(cls, parameter):
- """Returns the list of supported scatter visualization modes.
-
- See :meth:`VisualizationParameters`
-
- :param VisualizationParameter parameter:
- This parameter for which to retrieve the supported values.
- :returns: tuple of supported of values or None if not defined.
- """
- parameter = cls.VisualizationParameter(parameter)
- return cls._SUPPORTED_VISUALIZATION_PARAMETER_VALUES.get(
- parameter, None)
-
- def setVisualization(self, mode):
- """Set the scatter plot visualization mode to use.
-
- See :class:`Visualization` for all possible values,
- and :meth:`supportedVisualizations` for supported ones.
-
- :param Union[str,Visualization] mode:
- The visualization mode to use.
- :return: True if value was set, False if is was already set
- :rtype: bool
- """
- mode = self.Visualization.from_value(mode)
- assert mode in self.supportedVisualizations()
-
- if mode != self.__visualization:
- self.__visualization = mode
-
- self._updated(ItemChangedType.VISUALIZATION_MODE)
- return True
- else:
- return False
-
- def getVisualization(self):
- """Returns the scatter plot visualization mode in use.
-
- :rtype: Visualization
- """
- return self.__visualization
-
- def setVisualizationParameter(self, parameter, value=None):
- """Set the given visualization parameter.
-
- :param Union[str,VisualizationParameter] parameter:
- The name of the parameter to set
- :param value: The value to use for this parameter
- Set to None to automatically set the parameter
- :raises ValueError: If parameter is not supported
- :return: True if parameter was set, False if is was already set
- :rtype: bool
- :raise ValueError: If value is not supported
- """
- parameter = self.VisualizationParameter.from_value(parameter)
-
- if self.__parameters[parameter] != value:
- validValues = self.supportedVisualizationParameterValues(parameter)
- if validValues is not None and value not in validValues:
- raise ValueError("Unsupported parameter value: %s" % str(value))
-
- self.__parameters[parameter] = value
- self._updated(ItemChangedType.VISUALIZATION_MODE)
- return True
- return False
-
- def getVisualizationParameter(self, parameter):
- """Returns the value of the given visualization parameter.
-
- This method returns the parameter as set by
- :meth:`setVisualizationParameter`.
-
- :param parameter: The name of the parameter to retrieve
- :returns: The value previously set or None if automatically set
- :raises ValueError: If parameter is not supported
- """
- if parameter not in self.VisualizationParameter:
- raise ValueError("parameter not supported: %s", parameter)
-
- return self.__parameters[parameter]
-
- def getCurrentVisualizationParameter(self, parameter):
- """Returns the current value of the given visualization parameter.
-
- If the parameter was set by :meth:`setVisualizationParameter` to
- a value that is not None, this value is returned;
- else the current value that is automatically computed is returned.
-
- :param parameter: The name of the parameter to retrieve
- :returns: The current value (either set or automatically computed)
- :raises ValueError: If parameter is not supported
- """
- # Override in subclass to provide automatically computed parameters
- return self.getVisualizationParameter(parameter)
-
-
-class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
- """Base class for :class:`Curve` and :class:`Scatter`"""
- # note: _logFilterData must be overloaded if you overload
- # getData to change its signature
-
- _DEFAULT_Z_LAYER = 1
- """Default overlay layer for points,
- on top of images."""
-
- def __init__(self):
- DataItem.__init__(self)
- SymbolMixIn.__init__(self)
- AlphaMixIn.__init__(self)
- self._x = ()
- self._y = ()
- self._xerror = None
- self._yerror = None
-
- # Store filtered data for x > 0 and/or y > 0
- self._filteredCache = {}
- self._clippedCache = {}
-
- # Store bounds depending on axes filtering >0:
- # key is (isXPositiveFilter, isYPositiveFilter)
- self._boundsCache = {}
-
- @staticmethod
- def _logFilterError(value, error):
- """Filter/convert error values if they go <= 0.
-
- Replace error leading to negative values by nan
-
- :param numpy.ndarray value: 1D array of values
- :param numpy.ndarray error:
- Array of errors: scalar, N, Nx1 or 2xN or None.
- :return: Filtered error so error bars are never negative
- """
- if error is not None:
- # Convert Nx1 to N
- if error.ndim == 2 and error.shape[1] == 1 and len(value) != 1:
- error = numpy.ravel(error)
-
- # Supports error being scalar, N or 2xN array
- valueMinusError = value - numpy.atleast_2d(error)[0]
- errorClipped = numpy.isnan(valueMinusError)
- mask = numpy.logical_not(errorClipped)
- errorClipped[mask] = valueMinusError[mask] <= 0
-
- if numpy.any(errorClipped): # Need filtering
-
- # expand errorbars to 2xN
- if error.size == 1: # Scalar
- error = numpy.full(
- (2, len(value)), error, dtype=numpy.float64)
-
- elif error.ndim == 1: # N array
- newError = numpy.empty((2, len(value)),
- dtype=numpy.float64)
- newError[0,:] = error
- newError[1,:] = error
- error = newError
-
- elif error.size == 2 * len(value): # 2xN array
- error = numpy.array(
- error, copy=True, dtype=numpy.float64)
-
- else:
- _logger.error("Unhandled error array")
- return error
-
- error[0, errorClipped] = numpy.nan
-
- return error
-
- def _getClippingBoolArray(self, xPositive, yPositive):
- """Compute a boolean array to filter out points with negative
- coordinates on log axes.
-
- :param bool xPositive: True to filter arrays according to X coords.
- :param bool yPositive: True to filter arrays according to Y coords.
- :rtype: boolean numpy.ndarray
- """
- assert xPositive or yPositive
- if (xPositive, yPositive) not in self._clippedCache:
- xclipped, yclipped = False, False
-
- if xPositive:
- x = self.getXData(copy=False)
- with numpy.errstate(invalid='ignore'): # Ignore NaN warnings
- xclipped = x <= 0
-
- if yPositive:
- y = self.getYData(copy=False)
- with numpy.errstate(invalid='ignore'): # Ignore NaN warnings
- yclipped = y <= 0
-
- self._clippedCache[(xPositive, yPositive)] = \
- numpy.logical_or(xclipped, yclipped)
- return self._clippedCache[(xPositive, yPositive)]
-
- def _logFilterData(self, xPositive, yPositive):
- """Filter out values with x or y <= 0 on log axes
-
- :param bool xPositive: True to filter arrays according to X coords.
- :param bool yPositive: True to filter arrays according to Y coords.
- :return: The filter arrays or unchanged object if filtering not needed
- :rtype: (x, y, xerror, yerror)
- """
- x = self.getXData(copy=False)
- y = self.getYData(copy=False)
- xerror = self.getXErrorData(copy=False)
- yerror = self.getYErrorData(copy=False)
-
- if xPositive or yPositive:
- clipped = self._getClippingBoolArray(xPositive, yPositive)
-
- if numpy.any(clipped):
- # copy to keep original array and convert to float
- x = numpy.array(x, copy=True, dtype=numpy.float64)
- x[clipped] = numpy.nan
- y = numpy.array(y, copy=True, dtype=numpy.float64)
- y[clipped] = numpy.nan
-
- if xPositive and xerror is not None:
- xerror = self._logFilterError(x, xerror)
-
- if yPositive and yerror is not None:
- yerror = self._logFilterError(y, yerror)
-
- return x, y, xerror, yerror
-
- def _getBounds(self):
- if self.getXData(copy=False).size == 0: # Empty data
- return None
-
- plot = self.getPlot()
- if plot is not None:
- xPositive = plot.getXAxis()._isLogarithmic()
- yPositive = plot.getYAxis()._isLogarithmic()
- else:
- xPositive = False
- yPositive = False
-
- # TODO bounds do not take error bars into account
- if (xPositive, yPositive) not in self._boundsCache:
- # use the getData class method because instance method can be
- # overloaded to return additional arrays
- data = PointsBase.getData(self, copy=False, displayed=True)
- if len(data) == 5:
- # hack to avoid duplicating caching mechanism in Scatter
- # (happens when cached data is used, caching done using
- # Scatter._logFilterData)
- x, y, _xerror, _yerror = data[0], data[1], data[3], data[4]
- else:
- x, y, _xerror, _yerror = data
-
- xmin, xmax = min_max(x, finite=True)
- ymin, ymax = min_max(y, finite=True)
- self._boundsCache[(xPositive, yPositive)] = tuple([
- (bound if bound is not None else numpy.nan)
- for bound in (xmin, xmax, ymin, ymax)])
- return self._boundsCache[(xPositive, yPositive)]
-
- def _getCachedData(self):
- """Return cached filtered data if applicable,
- i.e. if any axis is in log scale.
- Return None if caching is not applicable."""
- plot = self.getPlot()
- if plot is not None:
- xPositive = plot.getXAxis()._isLogarithmic()
- yPositive = plot.getYAxis()._isLogarithmic()
- if xPositive or yPositive:
- # At least one axis has log scale, filter data
- if (xPositive, yPositive) not in self._filteredCache:
- self._filteredCache[(xPositive, yPositive)] = \
- self._logFilterData(xPositive, yPositive)
- return self._filteredCache[(xPositive, yPositive)]
- return None
-
- def getData(self, copy=True, displayed=False):
- """Returns the x, y values of the curve points and xerror, yerror
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :param bool displayed: True to only get curve points that are displayed
- in the plot. Default: False
- Note: If plot has log scale, negative points
- are not displayed.
- :returns: (x, y, xerror, yerror)
- :rtype: 4-tuple of numpy.ndarray
- """
- if displayed: # filter data according to plot state
- cached_data = self._getCachedData()
- if cached_data is not None:
- return cached_data
-
- return (self.getXData(copy),
- self.getYData(copy),
- self.getXErrorData(copy),
- self.getYErrorData(copy))
-
- def getXData(self, copy=True):
- """Returns the x coordinates of the data points
-
- :param copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: numpy.ndarray
- """
- return numpy.array(self._x, copy=copy)
-
- def getYData(self, copy=True):
- """Returns the y coordinates of the data points
-
- :param copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: numpy.ndarray
- """
- return numpy.array(self._y, copy=copy)
-
- def getXErrorData(self, copy=True):
- """Returns the x error of the points
-
- :param copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: numpy.ndarray, float or None
- """
- if isinstance(self._xerror, numpy.ndarray):
- return numpy.array(self._xerror, copy=copy)
- else:
- return self._xerror # float or None
-
- def getYErrorData(self, copy=True):
- """Returns the y error of the points
-
- :param copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: numpy.ndarray, float or None
- """
- if isinstance(self._yerror, numpy.ndarray):
- return numpy.array(self._yerror, copy=copy)
- else:
- return self._yerror # float or None
-
- def setData(self, x, y, xerror=None, yerror=None, copy=True):
- """Set the data of the curve.
-
- :param numpy.ndarray x: The data corresponding to the x coordinates.
- :param numpy.ndarray y: The data corresponding to the y coordinates.
- :param xerror: Values with the uncertainties on the x values
- :type xerror: A float, or a numpy.ndarray of float32.
- If it is an array, it can either be a 1D array of
- same length as the data or a 2D array with 2 rows
- of same length as the data: row 0 for positive errors,
- row 1 for negative errors.
- :param yerror: Values with the uncertainties on the y values.
- :type yerror: A float, or a numpy.ndarray of float32. See xerror.
- :param bool copy: True make a copy of the data (default),
- False to use provided arrays.
- """
- x = numpy.array(x, copy=copy)
- y = numpy.array(y, copy=copy)
- assert len(x) == len(y)
- assert x.ndim == y.ndim == 1
-
- # Convert complex data
- if numpy.iscomplexobj(x):
- _logger.warning(
- 'Converting x data to absolute value to plot it.')
- x = numpy.absolute(x)
- if numpy.iscomplexobj(y):
- _logger.warning(
- 'Converting y data to absolute value to plot it.')
- y = numpy.absolute(y)
-
- if xerror is not None:
- if isinstance(xerror, abc.Iterable):
- xerror = numpy.array(xerror, copy=copy)
- if numpy.iscomplexobj(xerror):
- _logger.warning(
- 'Converting xerror data to absolute value to plot it.')
- xerror = numpy.absolute(xerror)
- else:
- xerror = float(xerror)
- if yerror is not None:
- if isinstance(yerror, abc.Iterable):
- yerror = numpy.array(yerror, copy=copy)
- if numpy.iscomplexobj(yerror):
- _logger.warning(
- 'Converting yerror data to absolute value to plot it.')
- yerror = numpy.absolute(yerror)
- else:
- yerror = float(yerror)
- # TODO checks on xerror, yerror
- self._x, self._y = x, y
- self._xerror, self._yerror = xerror, yerror
-
- self._boundsCache = {} # Reset cached bounds
- self._filteredCache = {} # Reset cached filtered data
- self._clippedCache = {} # Reset cached clipped bool array
-
- self._boundsChanged()
- self._updated(ItemChangedType.DATA)
-
-
-class BaselineMixIn(object):
- """Base class for Baseline mix-in"""
-
- def __init__(self, baseline=None):
- self._baseline = baseline
-
- def _setBaseline(self, baseline):
- """
- Set baseline value
-
- :param baseline: baseline value(s)
- :type: Union[None,float,numpy.ndarray]
- """
- if (isinstance(baseline, abc.Iterable)):
- baseline = numpy.array(baseline)
- self._baseline = baseline
-
- def getBaseline(self, copy=True):
- """
-
- :param bool copy:
- :return: histogram baseline
- :rtype: Union[None,float,numpy.ndarray]
- """
- if isinstance(self._baseline, numpy.ndarray):
- return numpy.array(self._baseline, copy=True)
- else:
- return self._baseline
-
-
-class _Style:
- """Object which store styles"""
-
-
-class HighlightedMixIn(ItemMixInBase):
-
- def __init__(self):
- self._highlightStyle = self._DEFAULT_HIGHLIGHT_STYLE
- self._highlighted = False
-
- def isHighlighted(self):
- """Returns True if curve is highlighted.
-
- :rtype: bool
- """
- return self._highlighted
-
- def setHighlighted(self, highlighted):
- """Set the highlight state of the curve
-
- :param bool highlighted:
- """
- highlighted = bool(highlighted)
- if highlighted != self._highlighted:
- self._highlighted = highlighted
- # TODO inefficient: better to use backend's setCurveColor
- self._updated(ItemChangedType.HIGHLIGHTED)
-
- def getHighlightedStyle(self):
- """Returns the highlighted style in use
-
- :rtype: CurveStyle
- """
- return self._highlightStyle
-
- def setHighlightedStyle(self, style):
- """Set the style to use for highlighting
-
- :param CurveStyle style: New style to use
- """
- previous = self.getHighlightedStyle()
- if style != previous:
- assert isinstance(style, _Style)
- self._highlightStyle = style
- self._updated(ItemChangedType.HIGHLIGHTED_STYLE)
-
- # Backward compatibility event
- if previous.getColor() != style.getColor():
- self._updated(ItemChangedType.HIGHLIGHTED_COLOR)
diff --git a/silx/gui/plot/items/curve.py b/silx/gui/plot/items/curve.py
deleted file mode 100644
index 75e7f01..0000000
--- a/silx/gui/plot/items/curve.py
+++ /dev/null
@@ -1,326 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""This module provides the :class:`Curve` item of the :class:`Plot`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "24/04/2018"
-
-
-import logging
-
-import numpy
-import six
-
-from ....utils.deprecation import deprecated
-from ... import colors
-from .core import (PointsBase, LabelsMixIn, ColorMixIn, YAxisMixIn,
- FillMixIn, LineMixIn, SymbolMixIn, ItemChangedType,
- BaselineMixIn, HighlightedMixIn, _Style)
-
-
-_logger = logging.getLogger(__name__)
-
-
-class CurveStyle(_Style):
- """Object storing the style of a curve.
-
- Set a value to None to use the default
-
- :param color: Color
- :param Union[str,None] linestyle: Style of the line
- :param Union[float,None] linewidth: Width of the line
- :param Union[str,None] symbol: Symbol for markers
- :param Union[float,None] symbolsize: Size of the markers
- """
-
- def __init__(self, color=None, linestyle=None, linewidth=None,
- symbol=None, symbolsize=None):
- if color is None:
- self._color = None
- else:
- if isinstance(color, six.string_types):
- color = colors.rgba(color)
- else: # array-like expected
- color = numpy.array(color, copy=False)
- if color.ndim == 1: # Array is 1D, this is a single color
- color = colors.rgba(color)
- self._color = color
-
- if linestyle is not None:
- assert linestyle in LineMixIn.getSupportedLineStyles()
- self._linestyle = linestyle
-
- self._linewidth = None if linewidth is None else float(linewidth)
-
- if symbol is not None:
- assert symbol in SymbolMixIn.getSupportedSymbols()
- self._symbol = symbol
-
- self._symbolsize = None if symbolsize is None else float(symbolsize)
-
- def getColor(self, copy=True):
- """Returns the color or None if not set.
-
- :param bool copy: True to get a copy (default),
- False to get internal representation (do not modify!)
-
- :rtype: Union[List[float],None]
- """
- if isinstance(self._color, numpy.ndarray):
- return numpy.array(self._color, copy=copy)
- else:
- return self._color
-
- def getLineStyle(self):
- """Return the type of the line or None if not set.
-
- Type of line::
-
- - ' ' no line
- - '-' solid line
- - '--' dashed line
- - '-.' dash-dot line
- - ':' dotted line
-
- :rtype: Union[str,None]
- """
- return self._linestyle
-
- def getLineWidth(self):
- """Return the curve line width in pixels or None if not set.
-
- :rtype: Union[float,None]
- """
- return self._linewidth
-
- def getSymbol(self):
- """Return the point marker type.
-
- Marker type::
-
- - 'o' circle
- - '.' point
- - ',' pixel
- - '+' cross
- - 'x' x-cross
- - 'd' diamond
- - 's' square
-
- :rtype: Union[str,None]
- """
- return self._symbol
-
- def getSymbolSize(self):
- """Return the point marker size in points.
-
- :rtype: Union[float,None]
- """
- return self._symbolsize
-
- def __eq__(self, other):
- if isinstance(other, CurveStyle):
- return (numpy.array_equal(self.getColor(), other.getColor()) and
- self.getLineStyle() == other.getLineStyle() and
- self.getLineWidth() == other.getLineWidth() and
- self.getSymbol() == other.getSymbol() and
- self.getSymbolSize() == other.getSymbolSize())
- else:
- return False
-
-
-class Curve(PointsBase, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn,
- LineMixIn, BaselineMixIn, HighlightedMixIn):
- """Description of a curve"""
-
- _DEFAULT_Z_LAYER = 1
- """Default overlay layer for curves"""
-
- _DEFAULT_SELECTABLE = True
- """Default selectable state for curves"""
-
- _DEFAULT_LINEWIDTH = 1.
- """Default line width of the curve"""
-
- _DEFAULT_LINESTYLE = '-'
- """Default line style of the curve"""
-
- _DEFAULT_HIGHLIGHT_STYLE = CurveStyle(color='black')
- """Default highlight style of the item"""
-
- _DEFAULT_BASELINE = None
-
- def __init__(self):
- PointsBase.__init__(self)
- ColorMixIn.__init__(self)
- YAxisMixIn.__init__(self)
- FillMixIn.__init__(self)
- LabelsMixIn.__init__(self)
- LineMixIn.__init__(self)
- BaselineMixIn.__init__(self)
- HighlightedMixIn.__init__(self)
-
- self._setBaseline(Curve._DEFAULT_BASELINE)
-
- def _addBackendRenderer(self, backend):
- """Update backend renderer"""
- # Filter-out values <= 0
- xFiltered, yFiltered, xerror, yerror = self.getData(
- copy=False, displayed=True)
-
- if len(xFiltered) == 0 or not numpy.any(numpy.isfinite(xFiltered)):
- return None # No data to display, do not add renderer to backend
-
- style = self.getCurrentStyle()
-
- return backend.addCurve(xFiltered, yFiltered,
- color=style.getColor(),
- symbol=style.getSymbol(),
- linestyle=style.getLineStyle(),
- linewidth=style.getLineWidth(),
- yaxis=self.getYAxis(),
- xerror=xerror,
- yerror=yerror,
- fill=self.isFill(),
- alpha=self.getAlpha(),
- symbolsize=style.getSymbolSize(),
- baseline=self.getBaseline(copy=False))
-
- def __getitem__(self, item):
- """Compatibility with PyMca and silx <= 0.4.0"""
- if isinstance(item, slice):
- return [self[index] for index in range(*item.indices(5))]
- elif item == 0:
- return self.getXData(copy=False)
- elif item == 1:
- return self.getYData(copy=False)
- elif item == 2:
- return self.getName()
- elif item == 3:
- info = self.getInfo(copy=False)
- return {} if info is None else info
- elif item == 4:
- params = {
- 'info': self.getInfo(),
- 'color': self.getColor(),
- 'symbol': self.getSymbol(),
- 'linewidth': self.getLineWidth(),
- 'linestyle': self.getLineStyle(),
- 'xlabel': self.getXLabel(),
- 'ylabel': self.getYLabel(),
- 'yaxis': self.getYAxis(),
- 'xerror': self.getXErrorData(copy=False),
- 'yerror': self.getYErrorData(copy=False),
- 'z': self.getZValue(),
- 'selectable': self.isSelectable(),
- 'fill': self.isFill(),
- }
- return params
- else:
- raise IndexError("Index out of range: %s", str(item))
-
- @deprecated(replacement='Curve.getHighlightedStyle().getColor()',
- since_version='0.9.0')
- def getHighlightedColor(self):
- """Returns the RGBA highlight color of the item
-
- :rtype: 4-tuple of float in [0, 1]
- """
- return self.getHighlightedStyle().getColor()
-
- @deprecated(replacement='Curve.setHighlightedStyle()',
- since_version='0.9.0')
- def setHighlightedColor(self, color):
- """Set the color to use when highlighted
-
- :param color: color(s) to be used for highlight
- :type color: str ("#RRGGBB") or (npoints, 4) unsigned byte array or
- one of the predefined color names defined in colors.py
- """
- self.setHighlightedStyle(CurveStyle(color))
-
- def getCurrentStyle(self):
- """Returns the current curve style.
-
- Curve style depends on curve highlighting
-
- :rtype: CurveStyle
- """
- if self.isHighlighted():
- style = self.getHighlightedStyle()
- color = style.getColor()
- linestyle = style.getLineStyle()
- linewidth = style.getLineWidth()
- symbol = style.getSymbol()
- symbolsize = style.getSymbolSize()
-
- return CurveStyle(
- color=self.getColor() if color is None else color,
- linestyle=self.getLineStyle() if linestyle is None else linestyle,
- linewidth=self.getLineWidth() if linewidth is None else linewidth,
- symbol=self.getSymbol() if symbol is None else symbol,
- symbolsize=self.getSymbolSize() if symbolsize is None else symbolsize)
-
- else:
- return CurveStyle(color=self.getColor(),
- linestyle=self.getLineStyle(),
- linewidth=self.getLineWidth(),
- symbol=self.getSymbol(),
- symbolsize=self.getSymbolSize())
-
- @deprecated(replacement='Curve.getCurrentStyle()',
- since_version='0.9.0')
- def getCurrentColor(self):
- """Returns the current color of the curve.
-
- This color is either the color of the curve or the highlighted color,
- depending on the highlight state.
-
- :rtype: 4-tuple of float in [0, 1]
- """
- return self.getCurrentStyle().getColor()
-
- def setData(self, x, y, xerror=None, yerror=None, baseline=None, copy=True):
- """Set the data of the curve.
-
- :param numpy.ndarray x: The data corresponding to the x coordinates.
- :param numpy.ndarray y: The data corresponding to the y coordinates.
- :param xerror: Values with the uncertainties on the x values
- :type xerror: A float, or a numpy.ndarray of float32.
- If it is an array, it can either be a 1D array of
- same length as the data or a 2D array with 2 rows
- of same length as the data: row 0 for positive errors,
- row 1 for negative errors.
- :param yerror: Values with the uncertainties on the y values.
- :type yerror: A float, or a numpy.ndarray of float32. See xerror.
- :param baseline: curve baseline
- :type baseline: Union[None,float,numpy.ndarray]
- :param bool copy: True make a copy of the data (default),
- False to use provided arrays.
- """
- PointsBase.setData(self, x=x, y=y, xerror=xerror, yerror=yerror,
- copy=copy)
- self._setBaseline(baseline=baseline)
diff --git a/silx/gui/plot/items/histogram.py b/silx/gui/plot/items/histogram.py
deleted file mode 100644
index 16bbefa..0000000
--- a/silx/gui/plot/items/histogram.py
+++ /dev/null
@@ -1,389 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions::t
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""This module provides the :class:`Histogram` item of the :class:`Plot`.
-"""
-
-__authors__ = ["H. Payno", "T. Vincent"]
-__license__ = "MIT"
-__date__ = "28/08/2018"
-
-import logging
-import typing
-
-import numpy
-from collections import OrderedDict, namedtuple
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
-
-from ....utils.proxy import docstring
-from .core import (DataItem, AlphaMixIn, BaselineMixIn, ColorMixIn, FillMixIn,
- LineMixIn, YAxisMixIn, ItemChangedType, Item)
-from ._pick import PickingResult
-
-_logger = logging.getLogger(__name__)
-
-
-def _computeEdges(x, histogramType):
- """Compute the edges from a set of xs and a rule to generate the edges
-
- :param x: the x value of the curve to transform into an histogram
- :param histogramType: the type of histogram we wan't to generate.
- This define the way to center the histogram values compared to the
- curve value. Possible values can be::
-
- - 'left'
- - 'right'
- - 'center'
-
- :return: the edges for the given x and the histogramType
- """
- # for now we consider that the spaces between xs are constant
- edges = x.copy()
- if histogramType == 'left':
- width = 1
- if len(x) > 1:
- width = x[1] - x[0]
- edges = numpy.append(x[0] - width, edges)
- if histogramType == 'center':
- edges = _computeEdges(edges, 'right')
- widths = (edges[1:] - edges[0:-1]) / 2.0
- widths = numpy.append(widths, widths[-1])
- edges = edges - widths
- if histogramType == 'right':
- width = 1
- if len(x) > 1:
- width = x[-1] - x[-2]
- edges = numpy.append(edges, x[-1] + width)
-
- return edges
-
-
-def _getHistogramCurve(histogram, edges):
- """Returns the x and y value of a curve corresponding to the histogram
-
- :param numpy.ndarray histogram: The values of the histogram
- :param numpy.ndarray edges: The bin edges of the histogram
- :return: a tuple(x, y) which contains the value of the curve to use
- to display the histogram
- """
- assert len(histogram) + 1 == len(edges)
- x = numpy.empty(len(histogram) * 2, dtype=edges.dtype)
- y = numpy.empty(len(histogram) * 2, dtype=histogram.dtype)
- # Make a curve with stairs
- x[:-1:2] = edges[:-1]
- x[1::2] = edges[1:]
- y[:-1:2] = histogram
- y[1::2] = histogram
-
- return x, y
-
-
-# TODO: Yerror, test log scale
-class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
- LineMixIn, YAxisMixIn, BaselineMixIn):
- """Description of an histogram"""
-
- _DEFAULT_Z_LAYER = 1
- """Default overlay layer for histograms"""
-
- _DEFAULT_SELECTABLE = False
- """Default selectable state for histograms"""
-
- _DEFAULT_LINEWIDTH = 1.
- """Default line width of the histogram"""
-
- _DEFAULT_LINESTYLE = '-'
- """Default line style of the histogram"""
-
- _DEFAULT_BASELINE = None
-
- def __init__(self):
- DataItem.__init__(self)
- AlphaMixIn.__init__(self)
- BaselineMixIn.__init__(self)
- ColorMixIn.__init__(self)
- FillMixIn.__init__(self)
- LineMixIn.__init__(self)
- YAxisMixIn.__init__(self)
-
- self._histogram = ()
- self._edges = ()
- self._setBaseline(Histogram._DEFAULT_BASELINE)
-
- def _addBackendRenderer(self, backend):
- """Update backend renderer"""
- values, edges, baseline = self.getData(copy=False)
-
- if values.size == 0:
- return None # No data to display, do not add renderer
-
- if values.size == 0:
- return None # No data to display, do not add renderer to backend
-
- x, y = _getHistogramCurve(values, edges)
-
- # Filter-out values <= 0
- plot = self.getPlot()
- if plot is not None:
- xPositive = plot.getXAxis()._isLogarithmic()
- yPositive = plot.getYAxis()._isLogarithmic()
- else:
- xPositive = False
- yPositive = False
-
- if xPositive or yPositive:
- clipped = numpy.logical_or(
- (x <= 0) if xPositive else False,
- (y <= 0) if yPositive else False)
- # Make a copy and replace negative points by NaN
- x = numpy.array(x, dtype=numpy.float64)
- y = numpy.array(y, dtype=numpy.float64)
- x[clipped] = numpy.nan
- y[clipped] = numpy.nan
-
- return backend.addCurve(x, y,
- color=self.getColor(),
- symbol='',
- linestyle=self.getLineStyle(),
- linewidth=self.getLineWidth(),
- yaxis=self.getYAxis(),
- xerror=None,
- yerror=None,
- fill=self.isFill(),
- alpha=self.getAlpha(),
- baseline=baseline,
- symbolsize=1)
-
- def _getBounds(self):
- values, edges, baseline = self.getData(copy=False)
-
- plot = self.getPlot()
- if plot is not None:
- xPositive = plot.getXAxis()._isLogarithmic()
- yPositive = plot.getYAxis()._isLogarithmic()
- else:
- xPositive = False
- yPositive = False
-
- if xPositive or yPositive:
- values = numpy.array(values, copy=True, dtype=numpy.float64)
-
- if xPositive:
- # Replace edges <= 0 by NaN and corresponding values by NaN
- clipped_edges = (edges <= 0)
- edges = numpy.array(edges, copy=True, dtype=numpy.float64)
- edges[clipped_edges] = numpy.nan
- clipped_values = numpy.logical_or(clipped_edges[:-1],
- clipped_edges[1:])
- else:
- clipped_values = numpy.zeros_like(values, dtype=bool)
-
- if yPositive:
- # Replace values <= 0 by NaN, do not modify edges
- clipped_values = numpy.logical_or(clipped_values, values <= 0)
-
- values[clipped_values] = numpy.nan
-
- if yPositive:
- return (numpy.nanmin(edges),
- numpy.nanmax(edges),
- numpy.nanmin(values),
- numpy.nanmax(values))
-
- else: # No log scale on y axis, include 0 in bounds
- if numpy.all(numpy.isnan(values)):
- return None
- return (numpy.nanmin(edges),
- numpy.nanmax(edges),
- min(0, numpy.nanmin(values)),
- max(0, numpy.nanmax(values)))
-
- def __pickFilledHistogram(self, x: float, y: float) -> typing.Optional[PickingResult]:
- """Picking implementation for filled histogram
-
- :param x: X position in pixels
- :param y: Y position in pixels
- """
- if not self.isFill():
- return None
-
- plot = self.getPlot()
- if plot is None:
- return None
-
- xData, yData = plot.pixelToData(x, y, axis=self.getYAxis())
- xmin, xmax, ymin, ymax = self.getBounds()
- if not xmin < xData < xmax or not ymin < yData < ymax:
- return None # Outside bounding box
-
- # Check x
- edges = self.getBinEdgesData(copy=False)
- index = numpy.searchsorted(edges, (xData,), side='left')[0] - 1
- # Safe indexing in histogram values
- index = numpy.clip(index, 0, len(edges) - 2)
-
- # Check y
- baseline = self.getBaseline(copy=False)
- if baseline is None:
- baseline = 0 # Default value
-
- value = self.getValueData(copy=False)[index]
- if ((baseline <= value and baseline <= yData <= value) or
- (value < baseline and value <= yData <= baseline)):
- return PickingResult(self, numpy.array([index]))
- else:
- return None
-
- @docstring(DataItem)
- def pick(self, x, y):
- if self.isFill():
- return self.__pickFilledHistogram(x, y)
- else:
- result = super().pick(x, y)
- if result is None:
- return None
- else: # Convert from curve indices to histogram indices
- return PickingResult(self, numpy.unique(result.getIndices() // 2))
-
- def getValueData(self, copy=True):
- """The values of the histogram
-
- :param copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :returns: The values of the histogram
- :rtype: numpy.ndarray
- """
- return numpy.array(self._histogram, copy=copy)
-
- def getBinEdgesData(self, copy=True):
- """The bin edges of the histogram (number of histogram values + 1)
-
- :param copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :returns: The bin edges of the histogram
- :rtype: numpy.ndarray
- """
- return numpy.array(self._edges, copy=copy)
-
- def getData(self, copy=True):
- """Return the histogram values, bin edges and baseline
-
- :param copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :returns: (N histogram value, N+1 bin edges)
- :rtype: 2-tuple of numpy.nadarray
- """
- return (self.getValueData(copy),
- self.getBinEdgesData(copy),
- self.getBaseline(copy))
-
- def setData(self, histogram, edges, align='center', baseline=None,
- copy=True):
- """Set the histogram values and bin edges.
-
- :param numpy.ndarray histogram: The values of the histogram.
- :param numpy.ndarray edges:
- The bin edges of the histogram.
- If histogram and edges have the same length, the bin edges
- are computed according to the align parameter.
- :param str align:
- In case histogram values and edges have the same length N,
- the N+1 bin edges are computed according to the alignment in:
- 'center' (default), 'left', 'right'.
- :param baseline: histogram baseline
- :type baseline: Union[None,float,numpy.ndarray]
- :param bool copy: True make a copy of the data (default),
- False to use provided arrays.
- """
- histogram = numpy.array(histogram, copy=copy)
- edges = numpy.array(edges, copy=copy)
-
- assert histogram.ndim == 1
- assert edges.ndim == 1
- assert edges.size in (histogram.size, histogram.size + 1)
- assert align in ('center', 'left', 'right')
-
- if histogram.size == 0: # No data
- self._histogram = ()
- self._edges = ()
- else:
- if edges.size == histogram.size: # Compute true bin edges
- edges = _computeEdges(edges, align)
-
- # Check that bin edges are monotonic
- edgesDiff = numpy.diff(edges)
- edgesDiff = edgesDiff[numpy.logical_not(numpy.isnan(edgesDiff))]
- assert numpy.all(edgesDiff >= 0) or numpy.all(edgesDiff <= 0)
- # manage baseline
- if (isinstance(baseline, abc.Iterable)):
- baseline = numpy.array(baseline)
- if baseline.size == histogram.size:
- new_baseline = numpy.empty(baseline.shape[0] * 2)
- for i_value, value in enumerate(baseline):
- new_baseline[i_value*2:i_value*2+2] = value
- baseline = new_baseline
- self._histogram = histogram
- self._edges = edges
- self._alignement = align
- self._setBaseline(baseline)
-
- self._boundsChanged()
- self._updated(ItemChangedType.DATA)
-
- def getAlignment(self):
- """
-
- :return: histogram alignement. Value in ('center', 'left', 'right').
- """
- return self._alignement
-
- def _revertComputeEdges(self, x, histogramType):
- """Compute the edges from a set of xs and a rule to generate the edges
-
- :param x: the x value of the curve to transform into an histogram
- :param histogramType: the type of histogram we wan't to generate.
- This define the way to center the histogram values compared to the
- curve value. Possible values can be::
-
- - 'left'
- - 'right'
- - 'center'
-
- :return: the edges for the given x and the histogramType
- """
- # for now we consider that the spaces between xs are constant
- edges = x.copy()
- if histogramType == 'left':
- return edges[1:]
- if histogramType == 'center':
- edges = (edges[1:] + edges[:-1]) / 2.0
- if histogramType == 'right':
- width = 1
- if len(x) > 1:
- width = x[-1] + x[-2]
- edges = edges[:-1]
- return edges
diff --git a/silx/gui/plot/items/image.py b/silx/gui/plot/items/image.py
deleted file mode 100644
index 0d9c9a4..0000000
--- a/silx/gui/plot/items/image.py
+++ /dev/null
@@ -1,617 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""This module provides the :class:`ImageData` and :class:`ImageRgba` items
-of the :class:`Plot`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "08/12/2020"
-
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
-import logging
-
-import numpy
-
-from ....utils.proxy import docstring
-from .core import (DataItem, LabelsMixIn, DraggableMixIn, ColormapMixIn,
- AlphaMixIn, ItemChangedType)
-
-_logger = logging.getLogger(__name__)
-
-
-def _convertImageToRgba32(image, copy=True):
- """Convert an RGB or RGBA image to RGBA32.
-
- It converts from floats in [0, 1], bool, integer and uint in [0, 255]
-
- If the input image is already an RGBA32 image,
- the returned image shares the same data.
-
- :param image: Image to convert to
- :type image: numpy.ndarray with 3 dimensions: height, width, color channels
- :param bool copy: True (Default) to get a copy, False, avoid copy if possible
- :return: The image converted to RGBA32 with dimension: (height, width, 4)
- :rtype: numpy.ndarray of uint8
- """
- assert image.ndim == 3
- assert image.shape[-1] in (3, 4)
-
- # Convert type to uint8
- if image.dtype.name != 'uint8':
- if image.dtype.kind == 'f': # Float in [0, 1]
- image = (numpy.clip(image, 0., 1.) * 255).astype(numpy.uint8)
- elif image.dtype.kind == 'b': # boolean
- image = image.astype(numpy.uint8) * 255
- elif image.dtype.kind in ('i', 'u'): # int, uint
- image = numpy.clip(image, 0, 255).astype(numpy.uint8)
- else:
- raise ValueError('Unsupported image dtype: %s', image.dtype.name)
- copy = False # A copy as already been done, avoid next one
-
- # Convert RGB to RGBA
- if image.shape[-1] == 3:
- new_image = numpy.empty((image.shape[0], image.shape[1], 4),
- dtype=numpy.uint8)
- new_image[:,:,:3] = image
- new_image[:,:, 3] = 255
- return new_image # This is a copy anyway
- else:
- return numpy.array(image, copy=copy)
-
-
-class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
- """Description of an image
-
- :param numpy.ndarray data: Initial image data
- """
-
- def __init__(self, data=None, mask=None):
- DataItem.__init__(self)
- LabelsMixIn.__init__(self)
- DraggableMixIn.__init__(self)
- AlphaMixIn.__init__(self)
- if data is None:
- data = numpy.zeros((0, 0, 4), dtype=numpy.uint8)
- self._data = data
- self._mask = mask
- self.__valueDataCache = None # Store default data
- self._origin = (0., 0.)
- self._scale = (1., 1.)
-
- def __getitem__(self, item):
- """Compatibility with PyMca and silx <= 0.4.0"""
- if isinstance(item, slice):
- return [self[index] for index in range(*item.indices(5))]
- elif item == 0:
- return self.getData(copy=False)
- elif item == 1:
- return self.getName()
- elif item == 2:
- info = self.getInfo(copy=False)
- return {} if info is None else info
- elif item == 3:
- return None
- elif item == 4:
- params = {
- 'info': self.getInfo(),
- 'origin': self.getOrigin(),
- 'scale': self.getScale(),
- 'z': self.getZValue(),
- 'selectable': self.isSelectable(),
- 'draggable': self.isDraggable(),
- 'colormap': None,
- 'xlabel': self.getXLabel(),
- 'ylabel': self.getYLabel(),
- }
- return params
- else:
- raise IndexError("Index out of range: %s" % str(item))
-
- def _isPlotLinear(self, plot):
- """Return True if plot only uses linear scale for both of x and y
- axes."""
- linear = plot.getXAxis().LINEAR
- if plot.getXAxis().getScale() != linear:
- return False
- if plot.getYAxis().getScale() != linear:
- return False
- return True
-
- def _getBounds(self):
- if self.getData(copy=False).size == 0: # Empty data
- return None
-
- height, width = self.getData(copy=False).shape[:2]
- origin = self.getOrigin()
- scale = self.getScale()
- # Taking care of scale might be < 0
- xmin, xmax = origin[0], origin[0] + width * scale[0]
- if xmin > xmax:
- xmin, xmax = xmax, xmin
- # Taking care of scale might be < 0
- ymin, ymax = origin[1], origin[1] + height * scale[1]
- if ymin > ymax:
- ymin, ymax = ymax, ymin
-
- plot = self.getPlot()
- if plot is not None and not self._isPlotLinear(plot):
- return None
- else:
- return xmin, xmax, ymin, ymax
-
- @docstring(DraggableMixIn)
- def drag(self, from_, to):
- origin = self.getOrigin()
- self.setOrigin((origin[0] + to[0] - from_[0],
- origin[1] + to[1] - from_[1]))
-
- def getData(self, copy=True):
- """Returns the image data
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: numpy.ndarray
- """
- return numpy.array(self._data, copy=copy)
-
- def setData(self, data):
- """Set the image data
-
- :param numpy.ndarray data:
- """
- previousShape = self._data.shape
- self._data = data
- self._valueDataChanged()
- self._boundsChanged()
- self._updated(ItemChangedType.DATA)
-
- if (self.getMaskData(copy=False) is not None and
- previousShape != self._data.shape):
- # Data shape changed, so mask shape changes.
- # Send event, mask is lazily updated in getMaskData
- self._updated(ItemChangedType.MASK)
-
- def getMaskData(self, copy=True):
- """Returns the mask data
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: Union[None,numpy.ndarray]
- """
- if self._mask is None:
- return None
-
- # Update mask if it does not match data shape
- shape = self.getData(copy=False).shape[:2]
- if self._mask.shape != shape:
- # Clip/extend mask to match data
- newMask = numpy.zeros(shape, dtype=self._mask.dtype)
- newMask[:self._mask.shape[0], :self._mask.shape[1]] = self._mask[:shape[0], :shape[1]]
- self._mask = newMask
-
- return numpy.array(self._mask, copy=copy)
-
- def setMaskData(self, mask, copy=True):
- """Set the image data
-
- :param numpy.ndarray data:
- :param bool copy: True (Default) to make a copy,
- False to use as is (do not modify!)
- """
- if mask is not None:
- mask = numpy.array(mask, copy=copy)
-
- shape = self.getData(copy=False).shape[:2]
- if mask.shape != shape:
- _logger.warning("Inconsistent shape between mask and data %s, %s", mask.shape, shape)
- # Clip/extent is done lazily in getMaskData
- elif self._mask is None:
- return # No update
-
- self._mask = mask
- self._valueDataChanged()
- self._updated(ItemChangedType.MASK)
-
- def _valueDataChanged(self):
- """Clear cache of default data array"""
- self.__valueDataCache = None
-
- def _getValueData(self, copy=True):
- """Return data used by :meth:`getValueData`
-
- :param bool copy:
- :rtype: numpy.ndarray
- """
- return self.getData(copy=copy)
-
- def getValueData(self, copy=True):
- """Return data (converted to int or float) with mask applied.
-
- Masked values are set to Not-A-Number.
- It returns a 2D array of values (int or float).
-
- :param bool copy:
- :rtype: numpy.ndarray
- """
- if self.__valueDataCache is None:
- data = self._getValueData(copy=False)
- mask = self.getMaskData(copy=False)
- if mask is not None:
- if numpy.issubdtype(data.dtype, numpy.floating):
- dtype = data.dtype
- else:
- dtype = numpy.float64
- data = numpy.array(data, dtype=dtype, copy=True)
- data[mask != 0] = numpy.NaN
- self.__valueDataCache = data
- return numpy.array(self.__valueDataCache, copy=copy)
-
- def getRgbaImageData(self, copy=True):
- """Get the displayed RGB(A) image
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :returns: numpy.ndarray of uint8 of shape (height, width, 4)
- """
- raise NotImplementedError('This MUST be implemented in sub-class')
-
- def getOrigin(self):
- """Returns the offset from origin at which to display the image.
-
- :rtype: 2-tuple of float
- """
- return self._origin
-
- def setOrigin(self, origin):
- """Set the offset from origin at which to display the image.
-
- :param origin: (ox, oy) Offset from origin
- :type origin: float or 2-tuple of float
- """
- if isinstance(origin, abc.Sequence):
- origin = float(origin[0]), float(origin[1])
- else: # single value origin
- origin = float(origin), float(origin)
- if origin != self._origin:
- self._origin = origin
- self._boundsChanged()
- self._updated(ItemChangedType.POSITION)
-
- def getScale(self):
- """Returns the scale of the image in data coordinates.
-
- :rtype: 2-tuple of float
- """
- return self._scale
-
- def setScale(self, scale):
- """Set the scale of the image
-
- :param scale: (sx, sy) Scale of the image
- :type scale: float or 2-tuple of float
- """
- if isinstance(scale, abc.Sequence):
- scale = float(scale[0]), float(scale[1])
- else: # single value scale
- scale = float(scale), float(scale)
-
- if scale != self._scale:
- self._scale = scale
- self._boundsChanged()
- self._updated(ItemChangedType.SCALE)
-
-
-class ImageData(ImageBase, ColormapMixIn):
- """Description of a data image with a colormap"""
-
- def __init__(self):
- ImageBase.__init__(self, numpy.zeros((0, 0), dtype=numpy.float32))
- ColormapMixIn.__init__(self)
- self._alternativeImage = None
- self.__alpha = None
-
- def _addBackendRenderer(self, backend):
- """Update backend renderer"""
- plot = self.getPlot()
- assert plot is not None
- if not self._isPlotLinear(plot):
- # Do not render with non linear scales
- return None
-
- if (self.getAlternativeImageData(copy=False) is not None or
- self.getAlphaData(copy=False) is not None):
- dataToUse = self.getRgbaImageData(copy=False)
- else:
- dataToUse = self.getData(copy=False)
-
- if dataToUse.size == 0:
- return None # No data to display
-
- colormap = self.getColormap()
- if colormap.isAutoscale():
- # Avoid backend to compute autoscale: use item cache
- colormap = colormap.copy()
- colormap.setVRange(*colormap.getColormapRange(self))
-
- return backend.addImage(dataToUse,
- origin=self.getOrigin(),
- scale=self.getScale(),
- colormap=colormap,
- alpha=self.getAlpha())
-
- def __getitem__(self, item):
- """Compatibility with PyMca and silx <= 0.4.0"""
- if item == 3:
- return self.getAlternativeImageData(copy=False)
-
- params = ImageBase.__getitem__(self, item)
- if item == 4:
- params['colormap'] = self.getColormap()
-
- return params
-
- def getRgbaImageData(self, copy=True):
- """Get the displayed RGB(A) image
-
- :returns: Array of uint8 of shape (height, width, 4)
- :rtype: numpy.ndarray
- """
- alternative = self.getAlternativeImageData(copy=False)
- if alternative is not None:
- return _convertImageToRgba32(alternative, copy=copy)
- else:
- # Apply colormap, in this case an new array is always returned
- colormap = self.getColormap()
- image = colormap.applyToData(self)
- alphaImage = self.getAlphaData(copy=False)
- if alphaImage is not None:
- # Apply transparency
- image[:,:, 3] = image[:,:, 3] * alphaImage
- return image
-
- def getAlternativeImageData(self, copy=True):
- """Get the optional RGBA image that is displayed instead of the data
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: Union[None,numpy.ndarray]
- """
- if self._alternativeImage is None:
- return None
- else:
- return numpy.array(self._alternativeImage, copy=copy)
-
- def getAlphaData(self, copy=True):
- """Get the optional transparency image applied on the data
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: Union[None,numpy.ndarray]
- """
- if self.__alpha is None:
- return None
- else:
- return numpy.array(self.__alpha, copy=copy)
-
- def setData(self, data, alternative=None, alpha=None, copy=True):
- """"Set the image data and optionally an alternative RGB(A) representation
-
- :param numpy.ndarray data: Data array with 2 dimensions (h, w)
- :param alternative: RGB(A) image to display instead of data,
- shape: (h, w, 3 or 4)
- :type alternative: Union[None,numpy.ndarray]
- :param alpha: An array of transparency value in [0, 1] to use for
- display with shape: (h, w)
- :type alpha: Union[None,numpy.ndarray]
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- """
- data = numpy.array(data, copy=copy)
- assert data.ndim == 2
- if data.dtype.kind == 'b':
- _logger.warning(
- 'Converting boolean image to int8 to plot it.')
- data = numpy.array(data, copy=False, dtype=numpy.int8)
- elif numpy.iscomplexobj(data):
- _logger.warning(
- 'Converting complex image to absolute value to plot it.')
- data = numpy.absolute(data)
-
- if alternative is not None:
- alternative = numpy.array(alternative, copy=copy)
- assert alternative.ndim == 3
- assert alternative.shape[2] in (3, 4)
- assert alternative.shape[:2] == data.shape[:2]
- self._alternativeImage = alternative
-
- if alpha is not None:
- alpha = numpy.array(alpha, copy=copy)
- assert alpha.shape == data.shape
- if alpha.dtype.kind != 'f':
- alpha = alpha.astype(numpy.float32)
- if numpy.any(numpy.logical_or(alpha < 0., alpha > 1.)):
- alpha = numpy.clip(alpha, 0., 1.)
- self.__alpha = alpha
-
- super().setData(data)
-
- def _updated(self, event=None, checkVisibility=True):
- # Synchronizes colormapped data if changed
- if event in (ItemChangedType.DATA, ItemChangedType.MASK):
- self._setColormappedData(
- self.getValueData(copy=False),
- copy=False)
- super()._updated(event=event, checkVisibility=checkVisibility)
-
-
-class ImageRgba(ImageBase):
- """Description of an RGB(A) image"""
-
- def __init__(self):
- ImageBase.__init__(self, numpy.zeros((0, 0, 4), dtype=numpy.uint8))
-
- def _addBackendRenderer(self, backend):
- """Update backend renderer"""
- plot = self.getPlot()
- assert plot is not None
- if not self._isPlotLinear(plot):
- # Do not render with non linear scales
- return None
-
- data = self.getData(copy=False)
-
- if data.size == 0:
- return None # No data to display
-
- return backend.addImage(data,
- origin=self.getOrigin(),
- scale=self.getScale(),
- colormap=None,
- alpha=self.getAlpha())
-
- def getRgbaImageData(self, copy=True):
- """Get the displayed RGB(A) image
-
- :returns: numpy.ndarray of uint8 of shape (height, width, 4)
- """
- return _convertImageToRgba32(self.getData(copy=False), copy=copy)
-
- def setData(self, data, copy=True):
- """Set the image data
-
- :param data: RGB(A) image data to set
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- """
- data = numpy.array(data, copy=copy)
- assert data.ndim == 3
- assert data.shape[-1] in (3, 4)
- super().setData(data)
-
- def _getValueData(self, copy=True):
- """Compute the intensity of the RGBA image as default data.
-
- Conversion: https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion
-
- :param bool copy:
- """
- rgba = self.getRgbaImageData(copy=False).astype(numpy.float32)
- intensity = (rgba[:, :, 0] * 0.299 +
- rgba[:, :, 1] * 0.587 +
- rgba[:, :, 2] * 0.114)
- intensity *= rgba[:, :, 3] / 255.
- return intensity
-
-
-class MaskImageData(ImageData):
- """Description of an image used as a mask.
-
- This class is used to flag mask items. This information is used to improve
- internal silx widgets.
- """
- pass
-
-
-class ImageStack(ImageData):
- """Item to store a stack of images and to show it in the plot as one
- of the images of the stack.
-
- The stack is a 3D array ordered this way: `frame id, y, x`.
- So the first image of the stack can be reached this way: `stack[0, :, :]`
- """
-
- def __init__(self):
- ImageData.__init__(self)
- self.__stack = None
- """A 3D numpy array (or a mimic one, see ListOfImages)"""
- self.__stackPosition = None
- """Displayed position in the cube"""
-
- def setStackData(self, stack, position=None, copy=True):
- """Set the stack data
-
- :param stack: A 3D numpy array like
- :param int position: The position of the displayed image in the stack
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- """
- if self.__stack is stack:
- return
- if copy:
- stack = numpy.array(stack)
- assert stack.ndim == 3
- self.__stack = stack
- if position is not None:
- self.__stackPosition = position
- if self.__stackPosition is None:
- self.__stackPosition = 0
- self.__updateDisplayedData()
-
- def getStackData(self, copy=True):
- """Get the stored stack array.
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: A 3D numpy array, or numpy array like
- """
- if copy:
- return numpy.array(self.__stack)
- else:
- return self.__stack
-
- def setStackPosition(self, pos):
- """Set the displayed position on the stack.
-
- This function will clamp the stack position according to
- the real size of the first axis of the stack.
-
- :param int pos: A position on the first axis of the stack.
- """
- if self.__stackPosition == pos:
- return
- self.__stackPosition = pos
- self.__updateDisplayedData()
-
- def getStackPosition(self):
- """Get the displayed position of the stack.
-
- :rtype: int
- """
- return self.__stackPosition
-
- def __updateDisplayedData(self):
- """Update the displayed frame whenever the stack or the stack
- position are updated."""
- if self.__stack is None or self.__stackPosition is None:
- empty = numpy.array([]).reshape(0, 0)
- self.setData(empty, copy=False)
- return
- size = len(self.__stack)
- self.__stackPosition = numpy.clip(self.__stackPosition, 0, size)
- self.setData(self.__stack[self.__stackPosition], copy=False)
diff --git a/silx/gui/plot/items/marker.py b/silx/gui/plot/items/marker.py
deleted file mode 100755
index 50d070c..0000000
--- a/silx/gui/plot/items/marker.py
+++ /dev/null
@@ -1,281 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""This module provides markers item of the :class:`Plot`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "06/03/2017"
-
-
-import logging
-
-from ....utils.proxy import docstring
-from .core import (Item, DraggableMixIn, ColorMixIn, LineMixIn, SymbolMixIn,
- ItemChangedType, YAxisMixIn)
-from silx.gui import qt
-
-_logger = logging.getLogger(__name__)
-
-
-class MarkerBase(Item, DraggableMixIn, ColorMixIn, YAxisMixIn):
- """Base class for markers"""
-
- sigDragStarted = qt.Signal()
- """Signal emitted when the marker is pressed"""
- sigDragFinished = qt.Signal()
- """Signal emitted when the marker is released"""
-
- _DEFAULT_COLOR = (0., 0., 0., 1.)
- """Default color of the markers"""
-
- def __init__(self):
- Item.__init__(self)
- DraggableMixIn.__init__(self)
- ColorMixIn.__init__(self)
- YAxisMixIn.__init__(self)
-
- self._text = ''
- self._x = None
- self._y = None
- self._constraint = self._defaultConstraint
- self.__isBeingDragged = False
-
- def _addRendererCall(self, backend,
- symbol=None, linestyle='-', linewidth=1):
- """Perform the update of the backend renderer"""
- return backend.addMarker(
- x=self.getXPosition(),
- y=self.getYPosition(),
- text=self.getText(),
- color=self.getColor(),
- symbol=symbol,
- linestyle=linestyle,
- linewidth=linewidth,
- constraint=self.getConstraint(),
- yaxis=self.getYAxis())
-
- def _addBackendRenderer(self, backend):
- """Update backend renderer"""
- raise NotImplementedError()
-
- @docstring(DraggableMixIn)
- def drag(self, from_, to):
- self.setPosition(to[0], to[1])
-
- def isOverlay(self):
- """Returns True: A marker is always rendered as an overlay.
-
- :rtype: bool
- """
- return True
-
- def getText(self):
- """Returns marker text.
-
- :rtype: str
- """
- return self._text
-
- def setText(self, text):
- """Set the text of the marker.
-
- :param str text: The text to use
- """
- text = str(text)
- if text != self._text:
- self._text = text
- self._updated(ItemChangedType.TEXT)
-
- def getXPosition(self):
- """Returns the X position of the marker line in data coordinates
-
- :rtype: float or None
- """
- return self._x
-
- def getYPosition(self):
- """Returns the Y position of the marker line in data coordinates
-
- :rtype: float or None
- """
- return self._y
-
- def getPosition(self):
- """Returns the (x, y) position of the marker in data coordinates
-
- :rtype: 2-tuple of float or None
- """
- return self._x, self._y
-
- def setPosition(self, x, y):
- """Set marker position in data coordinates
-
- Constraint are applied if any.
-
- :param float x: X coordinates in data frame
- :param float y: Y coordinates in data frame
- """
- x, y = self.getConstraint()(x, y)
- x, y = float(x), float(y)
- if x != self._x or y != self._y:
- self._x, self._y = x, y
- self._updated(ItemChangedType.POSITION)
-
- def getConstraint(self):
- """Returns the dragging constraint of this item"""
- return self._constraint
-
- def _setConstraint(self, constraint): # TODO support update
- """Set the constraint.
-
- This is private for now as update is not handled.
-
- :param callable constraint:
- :param constraint: A function filtering item displacement by
- dragging operations or None for no filter.
- This function is called each time the item is
- moved.
- This is only used if isDraggable returns True.
- :type constraint: None or a callable that takes the coordinates of
- the current cursor position in the plot as input
- and that returns the filtered coordinates.
- """
- if constraint is None:
- constraint = self._defaultConstraint
- assert callable(constraint)
- self._constraint = constraint
-
- @staticmethod
- def _defaultConstraint(*args):
- """Default constraint not doing anything"""
- return args
-
- def _startDrag(self):
- self.__isBeingDragged = True
- self.sigDragStarted.emit()
-
- def _endDrag(self):
- self.__isBeingDragged = False
- self.sigDragFinished.emit()
-
- def isBeingDragged(self) -> bool:
- """Returns whether the marker is currently dragged by the user."""
- return self.__isBeingDragged
-
-
-class Marker(MarkerBase, SymbolMixIn):
- """Description of a marker"""
-
- _DEFAULT_SYMBOL = '+'
- """Default symbol of the marker"""
-
- def __init__(self):
- MarkerBase.__init__(self)
- SymbolMixIn.__init__(self)
-
- self._x = 0.
- self._y = 0.
-
- def _addBackendRenderer(self, backend):
- return self._addRendererCall(backend, symbol=self.getSymbol())
-
- def _setConstraint(self, constraint):
- """Set the constraint function of the marker drag.
-
- It also supports 'horizontal' and 'vertical' str as constraint.
-
- :param constraint: The constraint of the dragging of this marker
- :type: constraint: callable or str
- """
- if constraint == 'horizontal':
- constraint = self._horizontalConstraint
- elif constraint == 'vertical':
- constraint = self._verticalConstraint
-
- super(Marker, self)._setConstraint(constraint)
-
- def _horizontalConstraint(self, _, y):
- return self.getXPosition(), y
-
- def _verticalConstraint(self, x, _):
- return x, self.getYPosition()
-
-
-class _LineMarker(MarkerBase, LineMixIn):
- """Base class for line markers"""
-
- def __init__(self):
- MarkerBase.__init__(self)
- LineMixIn.__init__(self)
-
- def _addBackendRenderer(self, backend):
- return self._addRendererCall(backend,
- linestyle=self.getLineStyle(),
- linewidth=self.getLineWidth())
-
-
-class XMarker(_LineMarker):
- """Description of a marker"""
-
- def __init__(self):
- _LineMarker.__init__(self)
- self._x = 0.
-
- def setPosition(self, x, y):
- """Set marker line position in data coordinates
-
- Constraint are applied if any.
-
- :param float x: X coordinates in data frame
- :param float y: Y coordinates in data frame
- """
- x, _ = self.getConstraint()(x, y)
- x = float(x)
- if x != self._x:
- self._x = x
- self._updated(ItemChangedType.POSITION)
-
-
-class YMarker(_LineMarker):
- """Description of a marker"""
-
- def __init__(self):
- _LineMarker.__init__(self)
- self._y = 0.
-
- def setPosition(self, x, y):
- """Set marker line position in data coordinates
-
- Constraint are applied if any.
-
- :param float x: X coordinates in data frame
- :param float y: Y coordinates in data frame
- """
- _, y = self.getConstraint()(x, y)
- y = float(y)
- if y != self._y:
- self._y = y
- self._updated(ItemChangedType.POSITION)
diff --git a/silx/gui/plot/items/roi.py b/silx/gui/plot/items/roi.py
deleted file mode 100644
index 38a1424..0000000
--- a/silx/gui/plot/items/roi.py
+++ /dev/null
@@ -1,1519 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""This module provides ROI item for the :class:`~silx.gui.plot.PlotWidget`.
-
-.. inheritance-diagram::
- silx.gui.plot.items.roi
- :parts: 1
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "28/06/2018"
-
-
-import logging
-import numpy
-
-from ... import utils
-from .. import items
-from ...colors import rgba
-from silx.image.shapes import Polygon
-from silx.image._boundingbox import _BoundingBox
-from ....utils.proxy import docstring
-from ..utils.intersections import segments_intersection
-from ._roi_base import _RegionOfInterestBase
-
-# He following imports have to be exposed by this module
-from ._roi_base import RegionOfInterest
-from ._roi_base import HandleBasedROI
-from ._arc_roi import ArcROI # noqa
-from ._roi_base import InteractionModeMixIn # noqa
-from ._roi_base import RoiInteractionMode # noqa
-
-
-logger = logging.getLogger(__name__)
-
-
-class PointROI(RegionOfInterest, items.SymbolMixIn):
- """A ROI identifying a point in a 2D plot."""
-
- ICON = 'add-shape-point'
- NAME = 'point markers'
- SHORT_NAME = "point"
- """Metadata for this kind of ROI"""
-
- _plotShape = "point"
- """Plot shape which is used for the first interaction"""
-
- _DEFAULT_SYMBOL = '+'
- """Default symbol of the PointROI
-
- It overwrite the `SymbolMixIn` class attribte.
- """
-
- def __init__(self, parent=None):
- RegionOfInterest.__init__(self, parent=parent)
- items.SymbolMixIn.__init__(self)
- self._marker = items.Marker()
- self._marker.sigItemChanged.connect(self._pointPositionChanged)
- self._marker.setSymbol(self._DEFAULT_SYMBOL)
- self._marker.sigDragStarted.connect(self._editingStarted)
- self._marker.sigDragFinished.connect(self._editingFinished)
- self.addItem(self._marker)
-
- def setFirstShapePoints(self, points):
- self.setPosition(points[0])
-
- def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.NAME:
- label = self.getName()
- self._marker.setText(label)
- elif event == items.ItemChangedType.EDITABLE:
- self._marker._setDraggable(self.isEditable())
- elif event in [items.ItemChangedType.VISIBLE,
- items.ItemChangedType.SELECTABLE]:
- self._updateItemProperty(event, self, self._marker)
- super(PointROI, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- self._marker.setColor(style.getColor())
-
- def getPosition(self):
- """Returns the position of this ROI
-
- :rtype: numpy.ndarray
- """
- return self._marker.getPosition()
-
- def setPosition(self, pos):
- """Set the position of this ROI
-
- :param numpy.ndarray pos: 2d-coordinate of this point
- """
- self._marker.setPosition(*pos)
-
- @docstring(_RegionOfInterestBase)
- def contains(self, position):
- roiPos = self.getPosition()
- return position[0] == roiPos[0] and position[1] == roiPos[1]
-
- def _pointPositionChanged(self, event):
- """Handle position changed events of the marker"""
- if event is items.ItemChangedType.POSITION:
- self.sigRegionChanged.emit()
-
- def __str__(self):
- params = '%f %f' % self.getPosition()
- return "%s(%s)" % (self.__class__.__name__, params)
-
-
-class CrossROI(HandleBasedROI, items.LineMixIn):
- """A ROI identifying a point in a 2D plot and displayed as a cross
- """
-
- ICON = 'add-shape-cross'
- NAME = 'cross marker'
- SHORT_NAME = "cross"
- """Metadata for this kind of ROI"""
-
- _plotShape = "point"
- """Plot shape which is used for the first interaction"""
-
- def __init__(self, parent=None):
- HandleBasedROI.__init__(self, parent=parent)
- items.LineMixIn.__init__(self)
- self._handle = self.addHandle()
- self._handle.sigItemChanged.connect(self._handlePositionChanged)
- self._handleLabel = self.addLabelHandle()
- self._vmarker = self.addUserHandle(items.YMarker())
- self._vmarker._setSelectable(False)
- self._vmarker._setDraggable(False)
- self._vmarker.setPosition(*self.getPosition())
- self._hmarker = self.addUserHandle(items.XMarker())
- self._hmarker._setSelectable(False)
- self._hmarker._setDraggable(False)
- self._hmarker.setPosition(*self.getPosition())
-
- def _updated(self, event=None, checkVisibility=True):
- if event in [items.ItemChangedType.VISIBLE]:
- markers = (self._vmarker, self._hmarker)
- self._updateItemProperty(event, self, markers)
- super(CrossROI, self)._updated(event, checkVisibility)
-
- def _updateText(self, text):
- self._handleLabel.setText(text)
-
- def _updatedStyle(self, event, style):
- super(CrossROI, self)._updatedStyle(event, style)
- for marker in [self._vmarker, self._hmarker]:
- marker.setColor(style.getColor())
- marker.setLineStyle(style.getLineStyle())
- marker.setLineWidth(style.getLineWidth())
-
- def setFirstShapePoints(self, points):
- pos = points[0]
- self.setPosition(pos)
-
- def getPosition(self):
- """Returns the position of this ROI
-
- :rtype: numpy.ndarray
- """
- return self._handle.getPosition()
-
- def setPosition(self, pos):
- """Set the position of this ROI
-
- :param numpy.ndarray pos: 2d-coordinate of this point
- """
- self._handle.setPosition(*pos)
-
- def _handlePositionChanged(self, event):
- """Handle center marker position updates"""
- if event is items.ItemChangedType.POSITION:
- position = self.getPosition()
- self._handleLabel.setPosition(*position)
- self._vmarker.setPosition(*position)
- self._hmarker.setPosition(*position)
- self.sigRegionChanged.emit()
-
- @docstring(HandleBasedROI)
- def contains(self, position):
- roiPos = self.getPosition()
- return position[0] == roiPos[0] or position[1] == roiPos[1]
-
-
-class LineROI(HandleBasedROI, items.LineMixIn):
- """A ROI identifying a line in a 2D plot.
-
- This ROI provides 1 anchor for each boundary of the line, plus an center
- in the center to translate the full ROI.
- """
-
- ICON = 'add-shape-diagonal'
- NAME = 'line ROI'
- SHORT_NAME = "line"
- """Metadata for this kind of ROI"""
-
- _plotShape = "line"
- """Plot shape which is used for the first interaction"""
-
- def __init__(self, parent=None):
- HandleBasedROI.__init__(self, parent=parent)
- items.LineMixIn.__init__(self)
- self._handleStart = self.addHandle()
- self._handleEnd = self.addHandle()
- self._handleCenter = self.addTranslateHandle()
- self._handleLabel = self.addLabelHandle()
-
- shape = items.Shape("polylines")
- shape.setPoints([[0, 0], [0, 0]])
- shape.setColor(rgba(self.getColor()))
- shape.setFill(False)
- shape.setOverlay(True)
- shape.setLineStyle(self.getLineStyle())
- shape.setLineWidth(self.getLineWidth())
- self.__shape = shape
- self.addItem(shape)
-
- def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.VISIBLE:
- self._updateItemProperty(event, self, self.__shape)
- super(LineROI, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- super(LineROI, self)._updatedStyle(event, style)
- self.__shape.setColor(style.getColor())
- self.__shape.setLineStyle(style.getLineStyle())
- self.__shape.setLineWidth(style.getLineWidth())
-
- def setFirstShapePoints(self, points):
- assert len(points) == 2
- self.setEndPoints(points[0], points[1])
-
- def _updateText(self, text):
- self._handleLabel.setText(text)
-
- def setEndPoints(self, startPoint, endPoint):
- """Set this line location using the ending points
-
- :param numpy.ndarray startPoint: Staring bounding point of the line
- :param numpy.ndarray endPoint: Ending bounding point of the line
- """
- if not numpy.array_equal((startPoint, endPoint), self.getEndPoints()):
- self.__updateEndPoints(startPoint, endPoint)
-
- def __updateEndPoints(self, startPoint, endPoint):
- """Update marker and shape to match given end points
-
- :param numpy.ndarray startPoint: Staring bounding point of the line
- :param numpy.ndarray endPoint: Ending bounding point of the line
- """
- startPoint = numpy.array(startPoint)
- endPoint = numpy.array(endPoint)
- center = (startPoint + endPoint) * 0.5
-
- with utils.blockSignals(self._handleStart):
- self._handleStart.setPosition(startPoint[0], startPoint[1])
- with utils.blockSignals(self._handleEnd):
- self._handleEnd.setPosition(endPoint[0], endPoint[1])
- with utils.blockSignals(self._handleCenter):
- self._handleCenter.setPosition(center[0], center[1])
- with utils.blockSignals(self._handleLabel):
- self._handleLabel.setPosition(center[0], center[1])
-
- line = numpy.array((startPoint, endPoint))
- self.__shape.setPoints(line)
- self.sigRegionChanged.emit()
-
- def getEndPoints(self):
- """Returns bounding points of this ROI.
-
- :rtype: Tuple(numpy.ndarray,numpy.ndarray)
- """
- startPoint = numpy.array(self._handleStart.getPosition())
- endPoint = numpy.array(self._handleEnd.getPosition())
- return (startPoint, endPoint)
-
- def handleDragUpdated(self, handle, origin, previous, current):
- if handle is self._handleStart:
- _start, end = self.getEndPoints()
- self.__updateEndPoints(current, end)
- elif handle is self._handleEnd:
- start, _end = self.getEndPoints()
- self.__updateEndPoints(start, current)
- elif handle is self._handleCenter:
- start, end = self.getEndPoints()
- delta = current - previous
- start += delta
- end += delta
- self.setEndPoints(start, end)
-
- @docstring(_RegionOfInterestBase)
- def contains(self, position):
- bottom_left = position[0], position[1]
- bottom_right = position[0] + 1, position[1]
- top_left = position[0], position[1] + 1
- top_right = position[0] + 1, position[1] + 1
-
- points = self.__shape.getPoints()
- line_pt1 = points[0]
- line_pt2 = points[1]
-
- bb1 = _BoundingBox.from_points(points)
- if not bb1.contains(position):
- return False
-
- return (
- segments_intersection(seg1_start_pt=line_pt1, seg1_end_pt=line_pt2,
- seg2_start_pt=bottom_left, seg2_end_pt=bottom_right) or
- segments_intersection(seg1_start_pt=line_pt1, seg1_end_pt=line_pt2,
- seg2_start_pt=bottom_right, seg2_end_pt=top_right) or
- segments_intersection(seg1_start_pt=line_pt1, seg1_end_pt=line_pt2,
- seg2_start_pt=top_right, seg2_end_pt=top_left) or
- segments_intersection(seg1_start_pt=line_pt1, seg1_end_pt=line_pt2,
- seg2_start_pt=top_left, seg2_end_pt=bottom_left)
- ) is not None
-
- def __str__(self):
- start, end = self.getEndPoints()
- params = start[0], start[1], end[0], end[1]
- params = 'start: %f %f; end: %f %f' % params
- return "%s(%s)" % (self.__class__.__name__, params)
-
-
-class HorizontalLineROI(RegionOfInterest, items.LineMixIn):
- """A ROI identifying an horizontal line in a 2D plot."""
-
- ICON = 'add-shape-horizontal'
- NAME = 'horizontal line ROI'
- SHORT_NAME = "hline"
- """Metadata for this kind of ROI"""
-
- _plotShape = "hline"
- """Plot shape which is used for the first interaction"""
-
- def __init__(self, parent=None):
- RegionOfInterest.__init__(self, parent=parent)
- items.LineMixIn.__init__(self)
- self._marker = items.YMarker()
- self._marker.sigItemChanged.connect(self._linePositionChanged)
- self._marker.sigDragStarted.connect(self._editingStarted)
- self._marker.sigDragFinished.connect(self._editingFinished)
- self.addItem(self._marker)
-
- def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.NAME:
- label = self.getName()
- self._marker.setText(label)
- elif event == items.ItemChangedType.EDITABLE:
- self._marker._setDraggable(self.isEditable())
- elif event in [items.ItemChangedType.VISIBLE,
- items.ItemChangedType.SELECTABLE]:
- self._updateItemProperty(event, self, self._marker)
- super(HorizontalLineROI, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- self._marker.setColor(style.getColor())
- self._marker.setLineStyle(style.getLineStyle())
- self._marker.setLineWidth(style.getLineWidth())
-
- def setFirstShapePoints(self, points):
- pos = points[0, 1]
- if pos == self.getPosition():
- return
- self.setPosition(pos)
-
- def getPosition(self):
- """Returns the position of this line if the horizontal axis
-
- :rtype: float
- """
- pos = self._marker.getPosition()
- return pos[1]
-
- def setPosition(self, pos):
- """Set the position of this ROI
-
- :param float pos: Horizontal position of this line
- """
- self._marker.setPosition(0, pos)
-
- @docstring(_RegionOfInterestBase)
- def contains(self, position):
- return position[1] == self.getPosition()
-
- def _linePositionChanged(self, event):
- """Handle position changed events of the marker"""
- if event is items.ItemChangedType.POSITION:
- self.sigRegionChanged.emit()
-
- def __str__(self):
- params = 'y: %f' % self.getPosition()
- return "%s(%s)" % (self.__class__.__name__, params)
-
-
-class VerticalLineROI(RegionOfInterest, items.LineMixIn):
- """A ROI identifying a vertical line in a 2D plot."""
-
- ICON = 'add-shape-vertical'
- NAME = 'vertical line ROI'
- SHORT_NAME = "vline"
- """Metadata for this kind of ROI"""
-
- _plotShape = "vline"
- """Plot shape which is used for the first interaction"""
-
- def __init__(self, parent=None):
- RegionOfInterest.__init__(self, parent=parent)
- items.LineMixIn.__init__(self)
- self._marker = items.XMarker()
- self._marker.sigItemChanged.connect(self._linePositionChanged)
- self._marker.sigDragStarted.connect(self._editingStarted)
- self._marker.sigDragFinished.connect(self._editingFinished)
- self.addItem(self._marker)
-
- def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.NAME:
- label = self.getName()
- self._marker.setText(label)
- elif event == items.ItemChangedType.EDITABLE:
- self._marker._setDraggable(self.isEditable())
- elif event in [items.ItemChangedType.VISIBLE,
- items.ItemChangedType.SELECTABLE]:
- self._updateItemProperty(event, self, self._marker)
- super(VerticalLineROI, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- self._marker.setColor(style.getColor())
- self._marker.setLineStyle(style.getLineStyle())
- self._marker.setLineWidth(style.getLineWidth())
-
- def setFirstShapePoints(self, points):
- pos = points[0, 0]
- self.setPosition(pos)
-
- def getPosition(self):
- """Returns the position of this line if the horizontal axis
-
- :rtype: float
- """
- pos = self._marker.getPosition()
- return pos[0]
-
- def setPosition(self, pos):
- """Set the position of this ROI
-
- :param float pos: Horizontal position of this line
- """
- self._marker.setPosition(pos, 0)
-
- @docstring(RegionOfInterest)
- def contains(self, position):
- return position[0] == self.getPosition()
-
- def _linePositionChanged(self, event):
- """Handle position changed events of the marker"""
- if event is items.ItemChangedType.POSITION:
- self.sigRegionChanged.emit()
-
- def __str__(self):
- params = 'x: %f' % self.getPosition()
- return "%s(%s)" % (self.__class__.__name__, params)
-
-
-class RectangleROI(HandleBasedROI, items.LineMixIn):
- """A ROI identifying a rectangle in a 2D plot.
-
- This ROI provides 1 anchor for each corner, plus an anchor in the
- center to translate the full ROI.
- """
-
- ICON = 'add-shape-rectangle'
- NAME = 'rectangle ROI'
- SHORT_NAME = "rectangle"
- """Metadata for this kind of ROI"""
-
- _plotShape = "rectangle"
- """Plot shape which is used for the first interaction"""
-
- def __init__(self, parent=None):
- HandleBasedROI.__init__(self, parent=parent)
- items.LineMixIn.__init__(self)
- self._handleTopLeft = self.addHandle()
- self._handleTopRight = self.addHandle()
- self._handleBottomLeft = self.addHandle()
- self._handleBottomRight = self.addHandle()
- self._handleCenter = self.addTranslateHandle()
- self._handleLabel = self.addLabelHandle()
-
- shape = items.Shape("rectangle")
- shape.setPoints([[0, 0], [0, 0]])
- shape.setFill(False)
- shape.setOverlay(True)
- shape.setLineStyle(self.getLineStyle())
- shape.setLineWidth(self.getLineWidth())
- shape.setColor(rgba(self.getColor()))
- self.__shape = shape
- self.addItem(shape)
-
- def _updated(self, event=None, checkVisibility=True):
- if event in [items.ItemChangedType.VISIBLE]:
- self._updateItemProperty(event, self, self.__shape)
- super(RectangleROI, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- super(RectangleROI, self)._updatedStyle(event, style)
- self.__shape.setColor(style.getColor())
- self.__shape.setLineStyle(style.getLineStyle())
- self.__shape.setLineWidth(style.getLineWidth())
-
- def setFirstShapePoints(self, points):
- assert len(points) == 2
- self._setBound(points)
-
- def _setBound(self, points):
- """Initialize the rectangle from a bunch of points"""
- top = max(points[:, 1])
- bottom = min(points[:, 1])
- left = min(points[:, 0])
- right = max(points[:, 0])
- size = right - left, top - bottom
- self._updateGeometry(origin=(left, bottom), size=size)
-
- def _updateText(self, text):
- self._handleLabel.setText(text)
-
- def getCenter(self):
- """Returns the central point of this rectangle
-
- :rtype: numpy.ndarray([float,float])
- """
- pos = self._handleCenter.getPosition()
- return numpy.array(pos)
-
- def getOrigin(self):
- """Returns the corner point with the smaller coordinates
-
- :rtype: numpy.ndarray([float,float])
- """
- pos = self._handleBottomLeft.getPosition()
- return numpy.array(pos)
-
- def getSize(self):
- """Returns the size of this rectangle
-
- :rtype: numpy.ndarray([float,float])
- """
- vmin = self._handleBottomLeft.getPosition()
- vmax = self._handleTopRight.getPosition()
- vmin, vmax = numpy.array(vmin), numpy.array(vmax)
- return vmax - vmin
-
- def setOrigin(self, position):
- """Set the origin position of this ROI
-
- :param numpy.ndarray position: Location of the smaller corner of the ROI
- """
- size = self.getSize()
- self.setGeometry(origin=position, size=size)
-
- def setSize(self, size):
- """Set the size of this ROI
-
- :param numpy.ndarray size: Size of the center of the ROI
- """
- origin = self.getOrigin()
- self.setGeometry(origin=origin, size=size)
-
- def setCenter(self, position):
- """Set the size of this ROI
-
- :param numpy.ndarray position: Location of the center of the ROI
- """
- size = self.getSize()
- self.setGeometry(center=position, size=size)
-
- def setGeometry(self, origin=None, size=None, center=None):
- """Set the geometry of the ROI
- """
- if ((origin is None or numpy.array_equal(origin, self.getOrigin())) and
- (center is None or numpy.array_equal(center, self.getCenter())) and
- numpy.array_equal(size, self.getSize())):
- return # Nothing has changed
-
- self._updateGeometry(origin, size, center)
-
- def _updateGeometry(self, origin=None, size=None, center=None):
- """Forced update of the geometry of the ROI"""
- if origin is not None:
- origin = numpy.array(origin)
- size = numpy.array(size)
- points = numpy.array([origin, origin + size])
- center = origin + size * 0.5
- elif center is not None:
- center = numpy.array(center)
- size = numpy.array(size)
- points = numpy.array([center - size * 0.5, center + size * 0.5])
- else:
- raise ValueError("Origin or center expected")
-
- with utils.blockSignals(self._handleBottomLeft):
- self._handleBottomLeft.setPosition(points[0, 0], points[0, 1])
- with utils.blockSignals(self._handleBottomRight):
- self._handleBottomRight.setPosition(points[1, 0], points[0, 1])
- with utils.blockSignals(self._handleTopLeft):
- self._handleTopLeft.setPosition(points[0, 0], points[1, 1])
- with utils.blockSignals(self._handleTopRight):
- self._handleTopRight.setPosition(points[1, 0], points[1, 1])
- with utils.blockSignals(self._handleCenter):
- self._handleCenter.setPosition(center[0], center[1])
- with utils.blockSignals(self._handleLabel):
- self._handleLabel.setPosition(points[0, 0], points[0, 1])
-
- self.__shape.setPoints(points)
- self.sigRegionChanged.emit()
-
- @docstring(HandleBasedROI)
- def contains(self, position):
- assert isinstance(position, (tuple, list, numpy.array))
- points = self.__shape.getPoints()
- bb1 = _BoundingBox.from_points(points)
- return bb1.contains(position)
-
- def handleDragUpdated(self, handle, origin, previous, current):
- if handle is self._handleCenter:
- # It is the center anchor
- size = self.getSize()
- self._updateGeometry(center=current, size=size)
- else:
- opposed = {
- self._handleBottomLeft: self._handleTopRight,
- self._handleTopRight: self._handleBottomLeft,
- self._handleBottomRight: self._handleTopLeft,
- self._handleTopLeft: self._handleBottomRight,
- }
- handle2 = opposed[handle]
- current2 = handle2.getPosition()
- points = numpy.array([current, current2])
-
- # Switch handles if they were crossed by interaction
- if self._handleBottomLeft.getXPosition() > self._handleBottomRight.getXPosition():
- self._handleBottomLeft, self._handleBottomRight = self._handleBottomRight, self._handleBottomLeft
-
- if self._handleTopLeft.getXPosition() > self._handleTopRight.getXPosition():
- self._handleTopLeft, self._handleTopRight = self._handleTopRight, self._handleTopLeft
-
- if self._handleBottomLeft.getYPosition() > self._handleTopLeft.getYPosition():
- self._handleBottomLeft, self._handleTopLeft = self._handleTopLeft, self._handleBottomLeft
-
- if self._handleBottomRight.getYPosition() > self._handleTopRight.getYPosition():
- self._handleBottomRight, self._handleTopRight = self._handleTopRight, self._handleBottomRight
-
- self._setBound(points)
-
- def __str__(self):
- origin = self.getOrigin()
- w, h = self.getSize()
- params = origin[0], origin[1], w, h
- params = 'origin: %f %f; width: %f; height: %f' % params
- return "%s(%s)" % (self.__class__.__name__, params)
-
-
-class CircleROI(HandleBasedROI, items.LineMixIn):
- """A ROI identifying a circle in a 2D plot.
-
- This ROI provides 1 anchor at the center to translate the circle,
- and one anchor on the perimeter to change the radius.
- """
-
- ICON = 'add-shape-circle'
- NAME = 'circle ROI'
- SHORT_NAME = "circle"
- """Metadata for this kind of ROI"""
-
- _kind = "Circle"
- """Label for this kind of ROI"""
-
- _plotShape = "line"
- """Plot shape which is used for the first interaction"""
-
- def __init__(self, parent=None):
- items.LineMixIn.__init__(self)
- HandleBasedROI.__init__(self, parent=parent)
- self._handlePerimeter = self.addHandle()
- self._handleCenter = self.addTranslateHandle()
- self._handleCenter.sigItemChanged.connect(self._centerPositionChanged)
- self._handleLabel = self.addLabelHandle()
-
- shape = items.Shape("polygon")
- shape.setPoints([[0, 0], [0, 0]])
- shape.setColor(rgba(self.getColor()))
- shape.setFill(False)
- shape.setOverlay(True)
- shape.setLineStyle(self.getLineStyle())
- shape.setLineWidth(self.getLineWidth())
- self.__shape = shape
- self.addItem(shape)
-
- self.__radius = 0
-
- def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.VISIBLE:
- self._updateItemProperty(event, self, self.__shape)
- super(CircleROI, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- super(CircleROI, self)._updatedStyle(event, style)
- self.__shape.setColor(style.getColor())
- self.__shape.setLineStyle(style.getLineStyle())
- self.__shape.setLineWidth(style.getLineWidth())
-
- def setFirstShapePoints(self, points):
- assert len(points) == 2
- self._setRay(points)
-
- def _setRay(self, points):
- """Initialize the circle from the center point and a
- perimeter point."""
- center = points[0]
- radius = numpy.linalg.norm(points[0] - points[1])
- self.setGeometry(center=center, radius=radius)
-
- def _updateText(self, text):
- self._handleLabel.setText(text)
-
- def getCenter(self):
- """Returns the central point of this rectangle
-
- :rtype: numpy.ndarray([float,float])
- """
- pos = self._handleCenter.getPosition()
- return numpy.array(pos)
-
- def getRadius(self):
- """Returns the radius of this circle
-
- :rtype: float
- """
- return self.__radius
-
- def setCenter(self, position):
- """Set the center point of this ROI
-
- :param numpy.ndarray position: Location of the center of the circle
- """
- self._handleCenter.setPosition(*position)
-
- def setRadius(self, radius):
- """Set the size of this ROI
-
- :param float size: Radius of the circle
- """
- radius = float(radius)
- if radius != self.__radius:
- self.__radius = radius
- self._updateGeometry()
-
- def setGeometry(self, center, radius):
- """Set the geometry of the ROI
- """
- if numpy.array_equal(center, self.getCenter()):
- self.setRadius(radius)
- else:
- self.__radius = float(radius) # Update radius directly
- self.setCenter(center) # Calls _updateGeometry
-
- def _updateGeometry(self):
- """Update the handles and shape according to given parameters"""
- center = self.getCenter()
- perimeter_point = numpy.array([center[0] + self.__radius, center[1]])
-
- self._handlePerimeter.setPosition(perimeter_point[0], perimeter_point[1])
- self._handleLabel.setPosition(center[0], center[1])
-
- nbpoints = 27
- angles = numpy.arange(nbpoints) * 2.0 * numpy.pi / nbpoints
- circleShape = numpy.array((numpy.cos(angles) * self.__radius,
- numpy.sin(angles) * self.__radius)).T
- circleShape += center
- self.__shape.setPoints(circleShape)
- self.sigRegionChanged.emit()
-
- def _centerPositionChanged(self, event):
- """Handle position changed events of the center marker"""
- if event is items.ItemChangedType.POSITION:
- self._updateGeometry()
-
- def handleDragUpdated(self, handle, origin, previous, current):
- if handle is self._handlePerimeter:
- center = self.getCenter()
- self.setRadius(numpy.linalg.norm(center - current))
-
- @docstring(HandleBasedROI)
- def contains(self, position):
- return numpy.linalg.norm(self.getCenter() - position) <= self.getRadius()
-
- def __str__(self):
- center = self.getCenter()
- radius = self.getRadius()
- params = center[0], center[1], radius
- params = 'center: %f %f; radius: %f;' % params
- return "%s(%s)" % (self.__class__.__name__, params)
-
-
-class EllipseROI(HandleBasedROI, items.LineMixIn):
- """A ROI identifying an oriented ellipse in a 2D plot.
-
- This ROI provides 1 anchor at the center to translate the circle,
- and two anchors on the perimeter to modify the major-radius and
- minor-radius. These two anchors also allow to change the orientation.
- """
-
- ICON = 'add-shape-ellipse'
- NAME = 'ellipse ROI'
- SHORT_NAME = "ellipse"
- """Metadata for this kind of ROI"""
-
- _plotShape = "line"
- """Plot shape which is used for the first interaction"""
-
- def __init__(self, parent=None):
- items.LineMixIn.__init__(self)
- HandleBasedROI.__init__(self, parent=parent)
- self._handleAxis0 = self.addHandle()
- self._handleAxis1 = self.addHandle()
- self._handleCenter = self.addTranslateHandle()
- self._handleCenter.sigItemChanged.connect(self._centerPositionChanged)
- self._handleLabel = self.addLabelHandle()
-
- shape = items.Shape("polygon")
- shape.setPoints([[0, 0], [0, 0]])
- shape.setColor(rgba(self.getColor()))
- shape.setFill(False)
- shape.setOverlay(True)
- shape.setLineStyle(self.getLineStyle())
- shape.setLineWidth(self.getLineWidth())
- self.__shape = shape
- self.addItem(shape)
-
- self._radius = 0., 0.
- self._orientation = 0. # angle in radians between the X-axis and the _handleAxis0
-
- def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.VISIBLE:
- self._updateItemProperty(event, self, self.__shape)
- super(EllipseROI, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- super(EllipseROI, self)._updatedStyle(event, style)
- self.__shape.setColor(style.getColor())
- self.__shape.setLineStyle(style.getLineStyle())
- self.__shape.setLineWidth(style.getLineWidth())
-
- def setFirstShapePoints(self, points):
- assert len(points) == 2
- self._setRay(points)
-
- @staticmethod
- def _calculateOrientation(p0, p1):
- """return angle in radians between the vector p0-p1
- and the X axis
-
- :param p0: first point coordinates (x, y)
- :param p1: second point coordinates
- :return:
- """
- vector = (p1[0] - p0[0], p1[1] - p0[1])
- x_unit_vector = (1, 0)
- norm = numpy.linalg.norm(vector)
- if norm != 0:
- theta = numpy.arccos(numpy.dot(vector, x_unit_vector) / norm)
- else:
- theta = 0
- if vector[1] < 0:
- # arccos always returns values in range [0, pi]
- theta = 2 * numpy.pi - theta
- return theta
-
- def _setRay(self, points):
- """Initialize the circle from the center point and a
- perimeter point."""
- center = points[0]
- radius = numpy.linalg.norm(points[0] - points[1])
- orientation = self._calculateOrientation(points[0], points[1])
- self.setGeometry(center=center,
- radius=(radius, radius),
- orientation=orientation)
-
- def _updateText(self, text):
- self._handleLabel.setText(text)
-
- def getCenter(self):
- """Returns the central point of this rectangle
-
- :rtype: numpy.ndarray([float,float])
- """
- pos = self._handleCenter.getPosition()
- return numpy.array(pos)
-
- def getMajorRadius(self):
- """Returns the half-diameter of the major axis.
-
- :rtype: float
- """
- return max(self._radius)
-
- def getMinorRadius(self):
- """Returns the half-diameter of the minor axis.
-
- :rtype: float
- """
- return min(self._radius)
-
- def getOrientation(self):
- """Return angle in radians between the horizontal (X) axis
- and the major axis of the ellipse in [0, 2*pi[
-
- :rtype: float:
- """
- return self._orientation
-
- def setCenter(self, center):
- """Set the center point of this ROI
-
- :param numpy.ndarray position: Coordinates (X, Y) of the center
- of the ellipse
- """
- self._handleCenter.setPosition(*center)
-
- def setMajorRadius(self, radius):
- """Set the half-diameter of the major axis of the ellipse.
-
- :param float radius:
- Major radius of the ellipsis. Must be a positive value.
- """
- if self._radius[0] > self._radius[1]:
- newRadius = radius, self._radius[1]
- else:
- newRadius = self._radius[0], radius
- self.setGeometry(radius=newRadius)
-
- def setMinorRadius(self, radius):
- """Set the half-diameter of the minor axis of the ellipse.
-
- :param float radius:
- Minor radius of the ellipsis. Must be a positive value.
- """
- if self._radius[0] > self._radius[1]:
- newRadius = self._radius[0], radius
- else:
- newRadius = radius, self._radius[1]
- self.setGeometry(radius=newRadius)
-
- def setOrientation(self, orientation):
- """Rotate the ellipse
-
- :param float orientation: Angle in radians between the horizontal and
- the major axis.
- :return:
- """
- self.setGeometry(orientation=orientation)
-
- def setGeometry(self, center=None, radius=None, orientation=None):
- """
-
- :param center: (X, Y) coordinates
- :param float majorRadius:
- :param float minorRadius:
- :param float orientation: angle in radians between the major axis and the
- horizontal
- :return:
- """
- if center is None:
- center = self.getCenter()
-
- if radius is None:
- radius = self._radius
- else:
- radius = float(radius[0]), float(radius[1])
-
- if orientation is None:
- orientation = self._orientation
- else:
- # ensure that we store the orientation in range [0, 2*pi
- orientation = numpy.mod(orientation, 2 * numpy.pi)
-
- if (numpy.array_equal(center, self.getCenter()) or
- radius != self._radius or
- orientation != self._orientation):
-
- # Update parameters directly
- self._radius = radius
- self._orientation = orientation
-
- if numpy.array_equal(center, self.getCenter()):
- self._updateGeometry()
- else:
- # This will call _updateGeometry
- self.setCenter(center)
-
- def _updateGeometry(self):
- """Update shape and markers"""
- center = self.getCenter()
-
- orientation = self.getOrientation()
- if self._radius[1] > self._radius[0]:
- # _handleAxis1 is the major axis
- orientation -= numpy.pi / 2
-
- point0 = numpy.array([center[0] + self._radius[0] * numpy.cos(orientation),
- center[1] + self._radius[0] * numpy.sin(orientation)])
- point1 = numpy.array([center[0] - self._radius[1] * numpy.sin(orientation),
- center[1] + self._radius[1] * numpy.cos(orientation)])
- with utils.blockSignals(self._handleAxis0):
- self._handleAxis0.setPosition(*point0)
- with utils.blockSignals(self._handleAxis1):
- self._handleAxis1.setPosition(*point1)
- with utils.blockSignals(self._handleLabel):
- self._handleLabel.setPosition(*center)
-
- nbpoints = 27
- angles = numpy.arange(nbpoints) * 2.0 * numpy.pi / nbpoints
- X = (self._radius[0] * numpy.cos(angles) * numpy.cos(orientation)
- - self._radius[1] * numpy.sin(angles) * numpy.sin(orientation))
- Y = (self._radius[0] * numpy.cos(angles) * numpy.sin(orientation)
- + self._radius[1] * numpy.sin(angles) * numpy.cos(orientation))
-
- ellipseShape = numpy.array((X, Y)).T
- ellipseShape += center
- self.__shape.setPoints(ellipseShape)
- self.sigRegionChanged.emit()
-
- def handleDragUpdated(self, handle, origin, previous, current):
- if handle in (self._handleAxis0, self._handleAxis1):
- center = self.getCenter()
- orientation = self._calculateOrientation(center, current)
- distance = numpy.linalg.norm(center - current)
-
- if handle is self._handleAxis1:
- if self._radius[0] > distance:
- # _handleAxis1 is not the major axis, rotate -90 degrees
- orientation -= numpy.pi / 2
- radius = self._radius[0], distance
-
- else: # _handleAxis0
- if self._radius[1] > distance:
- # _handleAxis0 is not the major axis, rotate +90 degrees
- orientation += numpy.pi / 2
- radius = distance, self._radius[1]
-
- self.setGeometry(radius=radius, orientation=orientation)
-
- def _centerPositionChanged(self, event):
- """Handle position changed events of the center marker"""
- if event is items.ItemChangedType.POSITION:
- self._updateGeometry()
-
- @docstring(HandleBasedROI)
- def contains(self, position):
- major, minor = self.getMajorRadius(), self.getMinorRadius()
- delta = self.getOrientation()
- x, y = position - self.getCenter()
- return ((x*numpy.cos(delta) + y*numpy.sin(delta))**2/major**2 +
- (x*numpy.sin(delta) - y*numpy.cos(delta))**2/minor**2) <= 1
-
- def __str__(self):
- center = self.getCenter()
- major = self.getMajorRadius()
- minor = self.getMinorRadius()
- orientation = self.getOrientation()
- params = center[0], center[1], major, minor, orientation
- params = 'center: %f %f; major radius: %f: minor radius: %f; orientation: %f' % params
- return "%s(%s)" % (self.__class__.__name__, params)
-
-
-class PolygonROI(HandleBasedROI, items.LineMixIn):
- """A ROI identifying a closed polygon in a 2D plot.
-
- This ROI provides 1 anchor for each point of the polygon.
- """
-
- ICON = 'add-shape-polygon'
- NAME = 'polygon ROI'
- SHORT_NAME = "polygon"
- """Metadata for this kind of ROI"""
-
- _plotShape = "polygon"
- """Plot shape which is used for the first interaction"""
-
- def __init__(self, parent=None):
- HandleBasedROI.__init__(self, parent=parent)
- items.LineMixIn.__init__(self)
- self._handleLabel = self.addLabelHandle()
- self._handleCenter = self.addTranslateHandle()
- self._handlePoints = []
- self._points = numpy.empty((0, 2))
- self._handleClose = None
-
- self._polygon_shape = None
- shape = self.__createShape()
- self.__shape = shape
- self.addItem(shape)
-
- def _updated(self, event=None, checkVisibility=True):
- if event in [items.ItemChangedType.VISIBLE]:
- self._updateItemProperty(event, self, self.__shape)
- super(PolygonROI, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- super(PolygonROI, self)._updatedStyle(event, style)
- self.__shape.setColor(style.getColor())
- self.__shape.setLineStyle(style.getLineStyle())
- self.__shape.setLineWidth(style.getLineWidth())
- if self._handleClose is not None:
- color = self._computeHandleColor(style.getColor())
- self._handleClose.setColor(color)
-
- def __createShape(self, interaction=False):
- kind = "polygon" if not interaction else "polylines"
- shape = items.Shape(kind)
- shape.setPoints([[0, 0], [0, 0]])
- shape.setFill(False)
- shape.setOverlay(True)
- style = self.getCurrentStyle()
- shape.setLineStyle(style.getLineStyle())
- shape.setLineWidth(style.getLineWidth())
- shape.setColor(rgba(style.getColor()))
- return shape
-
- def setFirstShapePoints(self, points):
- if self._handleClose is not None:
- self._handleClose.setPosition(*points[0])
- self.setPoints(points)
-
- def creationStarted(self):
- """"Called when the ROI creation interaction was started.
- """
- # Handle to see where to close the polygon
- self._handleClose = self.addUserHandle()
- self._handleClose.setSymbol("o")
- color = self._computeHandleColor(rgba(self.getColor()))
- self._handleClose.setColor(color)
-
- # Hide the center while creating the first shape
- self._handleCenter.setSymbol("")
-
- # In interaction replace the polygon by a line, to display something unclosed
- self.removeItem(self.__shape)
- self.__shape = self.__createShape(interaction=True)
- self.__shape.setPoints(self._points)
- self.addItem(self.__shape)
-
- def isBeingCreated(self):
- """Returns true if the ROI is in creation step"""
- return self._handleClose is not None
-
- def creationFinalized(self):
- """"Called when the ROI creation interaction was finalized.
- """
- self.removeHandle(self._handleClose)
- self._handleClose = None
- self.removeItem(self.__shape)
- self.__shape = self.__createShape()
- self.__shape.setPoints(self._points)
- self.addItem(self.__shape)
- # Hide the center while creating the first shape
- self._handleCenter.setSymbol("+")
- for handle in self._handlePoints:
- handle.setSymbol("s")
-
- def _updateText(self, text):
- self._handleLabel.setText(text)
-
- def getPoints(self):
- """Returns the list of the points of this polygon.
-
- :rtype: numpy.ndarray
- """
- return self._points.copy()
-
- def setPoints(self, points):
- """Set the position of this ROI
-
- :param numpy.ndarray pos: 2d-coordinate of this point
- """
- assert(len(points.shape) == 2 and points.shape[1] == 2)
-
- if numpy.array_equal(points, self._points):
- return # Nothing has changed
-
- self._polygon_shape = None
-
- # Update the needed handles
- while len(self._handlePoints) != len(points):
- if len(self._handlePoints) < len(points):
- handle = self.addHandle()
- self._handlePoints.append(handle)
- if self.isBeingCreated():
- handle.setSymbol("")
- else:
- handle = self._handlePoints.pop(-1)
- self.removeHandle(handle)
-
- for handle, position in zip(self._handlePoints, points):
- with utils.blockSignals(handle):
- handle.setPosition(position[0], position[1])
-
- if len(points) > 0:
- if not self.isHandleBeingDragged():
- vmin = numpy.min(points, axis=0)
- vmax = numpy.max(points, axis=0)
- center = (vmax + vmin) * 0.5
- with utils.blockSignals(self._handleCenter):
- self._handleCenter.setPosition(center[0], center[1])
-
- num = numpy.argmin(points[:, 1])
- pos = points[num]
- with utils.blockSignals(self._handleLabel):
- self._handleLabel.setPosition(pos[0], pos[1])
-
- if len(points) == 0:
- self._points = numpy.empty((0, 2))
- else:
- self._points = points
- self.__shape.setPoints(self._points)
- self.sigRegionChanged.emit()
-
- def translate(self, x, y):
- points = self.getPoints()
- delta = numpy.array([x, y])
- self.setPoints(points)
- self.setPoints(points + delta)
-
- def handleDragUpdated(self, handle, origin, previous, current):
- if handle is self._handleCenter:
- delta = current - previous
- self.translate(delta[0], delta[1])
- else:
- points = self.getPoints()
- num = self._handlePoints.index(handle)
- points[num] = current
- self.setPoints(points)
-
- def handleDragFinished(self, handle, origin, current):
- points = self._points
- if len(points) > 0:
- # Only update the center at the end
- # To avoid to disturb the interaction
- vmin = numpy.min(points, axis=0)
- vmax = numpy.max(points, axis=0)
- center = (vmax + vmin) * 0.5
- with utils.blockSignals(self._handleCenter):
- self._handleCenter.setPosition(center[0], center[1])
-
- def __str__(self):
- points = self._points
- params = '; '.join('%f %f' % (pt[0], pt[1]) for pt in points)
- return "%s(%s)" % (self.__class__.__name__, params)
-
- @docstring(HandleBasedROI)
- def contains(self, position):
- bb1 = _BoundingBox.from_points(self.getPoints())
- if bb1.contains(position) is False:
- return False
-
- if self._polygon_shape is None:
- self._polygon_shape = Polygon(vertices=self.getPoints())
-
- # warning: both the polygon and the value are inverted
- return self._polygon_shape.is_inside(row=position[0], col=position[1])
-
- def _setControlPoints(self, points):
- RegionOfInterest._setControlPoints(self, points=points)
- self._polygon_shape = None
-
-
-class HorizontalRangeROI(RegionOfInterest, items.LineMixIn):
- """A ROI identifying an horizontal range in a 1D plot."""
-
- ICON = 'add-range-horizontal'
- NAME = 'horizontal range ROI'
- SHORT_NAME = "hrange"
-
- _plotShape = "line"
- """Plot shape which is used for the first interaction"""
-
- def __init__(self, parent=None):
- RegionOfInterest.__init__(self, parent=parent)
- items.LineMixIn.__init__(self)
- self._markerMin = items.XMarker()
- self._markerMax = items.XMarker()
- self._markerCen = items.XMarker()
- self._markerCen.setLineStyle(" ")
- self._markerMin._setConstraint(self.__positionMinConstraint)
- self._markerMax._setConstraint(self.__positionMaxConstraint)
- self._markerMin.sigDragStarted.connect(self._editingStarted)
- self._markerMin.sigDragFinished.connect(self._editingFinished)
- self._markerMax.sigDragStarted.connect(self._editingStarted)
- self._markerMax.sigDragFinished.connect(self._editingFinished)
- self._markerCen.sigDragStarted.connect(self._editingStarted)
- self._markerCen.sigDragFinished.connect(self._editingFinished)
- self.addItem(self._markerCen)
- self.addItem(self._markerMin)
- self.addItem(self._markerMax)
- self.__filterReentrant = utils.LockReentrant()
-
- def setFirstShapePoints(self, points):
- vmin = min(points[:, 0])
- vmax = max(points[:, 0])
- self._updatePos(vmin, vmax)
-
- def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.NAME:
- self._updateText()
- elif event == items.ItemChangedType.EDITABLE:
- self._updateEditable()
- self._updateText()
- elif event == items.ItemChangedType.LINE_STYLE:
- markers = [self._markerMin, self._markerMax]
- self._updateItemProperty(event, self, markers)
- elif event in [items.ItemChangedType.VISIBLE,
- items.ItemChangedType.SELECTABLE]:
- markers = [self._markerMin, self._markerMax, self._markerCen]
- self._updateItemProperty(event, self, markers)
- super(HorizontalRangeROI, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- markers = [self._markerMin, self._markerMax, self._markerCen]
- for m in markers:
- m.setColor(style.getColor())
- m.setLineWidth(style.getLineWidth())
-
- def _updateText(self):
- text = self.getName()
- if self.isEditable():
- self._markerMin.setText("")
- self._markerCen.setText(text)
- else:
- self._markerMin.setText(text)
- self._markerCen.setText("")
-
- def _updateEditable(self):
- editable = self.isEditable()
- self._markerMin._setDraggable(editable)
- self._markerMax._setDraggable(editable)
- self._markerCen._setDraggable(editable)
- if self.isEditable():
- self._markerMin.sigItemChanged.connect(self._minPositionChanged)
- self._markerMax.sigItemChanged.connect(self._maxPositionChanged)
- self._markerCen.sigItemChanged.connect(self._cenPositionChanged)
- self._markerCen.setLineStyle(":")
- else:
- self._markerMin.sigItemChanged.disconnect(self._minPositionChanged)
- self._markerMax.sigItemChanged.disconnect(self._maxPositionChanged)
- self._markerCen.sigItemChanged.disconnect(self._cenPositionChanged)
- self._markerCen.setLineStyle(" ")
-
- def _updatePos(self, vmin, vmax, force=False):
- """Update marker position and emit signal.
-
- :param float vmin:
- :param float vmax:
- :param bool force:
- True to update even if already at the right position.
- """
- if not force and numpy.array_equal((vmin, vmax), self.getRange()):
- return # Nothing has changed
-
- center = (vmin + vmax) * 0.5
- with self.__filterReentrant:
- with utils.blockSignals(self._markerMin):
- self._markerMin.setPosition(vmin, 0)
- with utils.blockSignals(self._markerCen):
- self._markerCen.setPosition(center, 0)
- with utils.blockSignals(self._markerMax):
- self._markerMax.setPosition(vmax, 0)
- self.sigRegionChanged.emit()
-
- def setRange(self, vmin, vmax):
- """Set the range of this ROI.
-
- :param float vmin: Staring location of the range
- :param float vmax: Ending location of the range
- """
- if vmin is None or vmax is None:
- err = "Can't set vmin or vmax to None"
- raise ValueError(err)
- if vmin > vmax:
- err = "Can't set vmin and vmax because vmin >= vmax " \
- "vmin = %s, vmax = %s" % (vmin, vmax)
- raise ValueError(err)
- self._updatePos(vmin, vmax)
-
- def getRange(self):
- """Returns the range of this ROI.
-
- :rtype: Tuple[float,float]
- """
- vmin = self.getMin()
- vmax = self.getMax()
- return vmin, vmax
-
- def setMin(self, vmin):
- """Set the min of this ROI.
-
- :param float vmin: New min
- """
- vmax = self.getMax()
- self._updatePos(vmin, vmax)
-
- def getMin(self):
- """Returns the min value of this ROI.
-
- :rtype: float
- """
- return self._markerMin.getPosition()[0]
-
- def setMax(self, vmax):
- """Set the max of this ROI.
-
- :param float vmax: New max
- """
- vmin = self.getMin()
- self._updatePos(vmin, vmax)
-
- def getMax(self):
- """Returns the max value of this ROI.
-
- :rtype: float
- """
- return self._markerMax.getPosition()[0]
-
- def setCenter(self, center):
- """Set the center of this ROI.
-
- :param float center: New center
- """
- vmin, vmax = self.getRange()
- previousCenter = (vmin + vmax) * 0.5
- delta = center - previousCenter
- self._updatePos(vmin + delta, vmax + delta)
-
- def getCenter(self):
- """Returns the center location of this ROI.
-
- :rtype: float
- """
- vmin, vmax = self.getRange()
- return (vmin + vmax) * 0.5
-
- def __positionMinConstraint(self, x, y):
- """Constraint of the min marker"""
- if self.__filterReentrant.locked():
- # Ignore the constraint when we set an explicit value
- return x, y
- vmax = self.getMax()
- if vmax is None:
- return x, y
- return min(x, vmax), y
-
- def __positionMaxConstraint(self, x, y):
- """Constraint of the max marker"""
- if self.__filterReentrant.locked():
- # Ignore the constraint when we set an explicit value
- return x, y
- vmin = self.getMin()
- if vmin is None:
- return x, y
- return max(x, vmin), y
-
- def _minPositionChanged(self, event):
- """Handle position changed events of the marker"""
- if event is items.ItemChangedType.POSITION:
- marker = self.sender()
- self._updatePos(marker.getXPosition(), self.getMax(), force=True)
-
- def _maxPositionChanged(self, event):
- """Handle position changed events of the marker"""
- if event is items.ItemChangedType.POSITION:
- marker = self.sender()
- self._updatePos(self.getMin(), marker.getXPosition(), force=True)
-
- def _cenPositionChanged(self, event):
- """Handle position changed events of the marker"""
- if event is items.ItemChangedType.POSITION:
- marker = self.sender()
- self.setCenter(marker.getXPosition())
-
- @docstring(HandleBasedROI)
- def contains(self, position):
- return self.getMin() <= position[0] <= self.getMax()
-
- def __str__(self):
- vrange = self.getRange()
- params = 'min: %f; max: %f' % vrange
- return "%s(%s)" % (self.__class__.__name__, params)
diff --git a/silx/gui/plot/items/scatter.py b/silx/gui/plot/items/scatter.py
deleted file mode 100644
index 2d54223..0000000
--- a/silx/gui/plot/items/scatter.py
+++ /dev/null
@@ -1,973 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""This module provides the :class:`Scatter` item of the :class:`Plot`.
-"""
-
-from __future__ import division
-
-
-__authors__ = ["T. Vincent", "P. Knobel"]
-__license__ = "MIT"
-__date__ = "29/03/2017"
-
-
-from collections import namedtuple
-import logging
-import threading
-import numpy
-
-from collections import defaultdict
-from concurrent.futures import ThreadPoolExecutor, CancelledError
-
-from ....utils.proxy import docstring
-from ....math.combo import min_max
-from ....math.histogram import Histogramnd
-from ....utils.weakref import WeakList
-from .._utils.delaunay import delaunay
-from .core import PointsBase, ColormapMixIn, ScatterVisualizationMixIn
-from .axis import Axis
-from ._pick import PickingResult
-
-
-_logger = logging.getLogger(__name__)
-
-
-class _GreedyThreadPoolExecutor(ThreadPoolExecutor):
- """:class:`ThreadPoolExecutor` with an extra :meth:`submit_greedy` method.
- """
-
- def __init__(self, *args, **kwargs):
- super(_GreedyThreadPoolExecutor, self).__init__(*args, **kwargs)
- self.__futures = defaultdict(WeakList)
- self.__lock = threading.RLock()
-
- def submit_greedy(self, queue, fn, *args, **kwargs):
- """Same as :meth:`submit` but cancel previous tasks in given queue.
-
- This means that when a new task is submitted for a given queue,
- all other pending tasks of that queue are cancelled.
-
- :param queue: Identifier of the queue. This must be hashable.
- :param callable fn: The callable to call with provided extra arguments
- :return: Future corresponding to this task
- :rtype: concurrent.futures.Future
- """
- with self.__lock:
- # Cancel previous tasks in given queue
- for future in self.__futures.pop(queue, []):
- if not future.done():
- future.cancel()
-
- future = super(_GreedyThreadPoolExecutor, self).submit(
- fn, *args, **kwargs)
- self.__futures[queue].append(future)
-
- return future
-
-
-# Functions to guess grid shape from coordinates
-
-def _get_z_line_length(array):
- """Return length of line if array is a Z-like 2D regular grid.
-
- :param numpy.ndarray array: The 1D array of coordinates to check
- :return: 0 if no line length could be found,
- else the number of element per line.
- :rtype: int
- """
- sign = numpy.sign(numpy.diff(array))
- if len(sign) == 0 or sign[0] == 0: # We don't handle that
- return 0
- # Check this way to account for 0 sign (i.e., diff == 0)
- beginnings = numpy.where(sign == - sign[0])[0] + 1
- if len(beginnings) == 0:
- return 0
- length = beginnings[0]
- if numpy.all(numpy.equal(numpy.diff(beginnings), length)):
- return length
- return 0
-
-
-def _guess_z_grid_shape(x, y):
- """Guess the shape of a grid from (x, y) coordinates.
-
- The grid might contain more elements than x and y,
- as the last line might be partly filled.
-
- :param numpy.ndarray x:
- :paran numpy.ndarray y:
- :returns: (order, (height, width)) of the regular grid,
- or None if could not guess one.
- 'order' is 'row' if X (i.e., column) is the fast dimension, else 'column'.
- :rtype: Union[List(str,int),None]
- """
- width = _get_z_line_length(x)
- if width != 0:
- return 'row', (int(numpy.ceil(len(x) / width)), width)
- else:
- height = _get_z_line_length(y)
- if height != 0:
- return 'column', (height, int(numpy.ceil(len(y) / height)))
- return None
-
-
-def is_monotonic(array):
- """Returns whether array is monotonic (increasing or decreasing).
-
- :param numpy.ndarray array: 1D array-like container.
- :returns: 1 if array is monotonically increasing,
- -1 if array is monotonically decreasing,
- 0 if array is not monotonic
- :rtype: int
- """
- diff = numpy.diff(numpy.ravel(array))
- with numpy.errstate(invalid='ignore'):
- if numpy.all(diff >= 0):
- return 1
- elif numpy.all(diff <= 0):
- return -1
- else:
- return 0
-
-
-def _guess_grid(x, y):
- """Guess a regular grid from the points.
-
- Result convention is (x, y)
-
- :param numpy.ndarray x: X coordinates of the points
- :param numpy.ndarray y: Y coordinates of the points
- :returns: (order, (height, width)
- order is 'row' or 'column'
- :rtype: Union[List[str,List[int]],None]
- """
- x, y = numpy.ravel(x), numpy.ravel(y)
-
- guess = _guess_z_grid_shape(x, y)
- if guess is not None:
- return guess
-
- else:
- # Cannot guess a regular grid
- # Let's assume it's a single line
- order = 'row' # or 'column' doesn't matter for a single line
- y_monotonic = is_monotonic(y)
- if is_monotonic(x) or y_monotonic: # we can guess a line
- x_min, x_max = min_max(x)
- y_min, y_max = min_max(y)
-
- if not y_monotonic or x_max - x_min >= y_max - y_min:
- # x only is monotonic or both are and X varies more
- # line along X
- shape = 1, len(x)
- else:
- # y only is monotonic or both are and Y varies more
- # line along Y
- shape = len(y), 1
-
- else: # Cannot guess a line from the points
- return None
-
- return order, shape
-
-
-def _quadrilateral_grid_coords(points):
- """Compute an irregular grid of quadrilaterals from a set of points
-
- The input points are expected to lie on a grid.
-
- :param numpy.ndarray points:
- 3D data set of 2D input coordinates (height, width, 2)
- height and width must be at least 2.
- :return: 3D dataset of 2D coordinates of the grid (height+1, width+1, 2)
- """
- assert points.ndim == 3
- assert points.shape[0] >= 2
- assert points.shape[1] >= 2
- assert points.shape[2] == 2
-
- dim0, dim1 = points.shape[:2]
- grid_points = numpy.zeros((dim0 + 1, dim1 + 1, 2), dtype=numpy.float64)
-
- # Compute inner points as mean of 4 neighbours
- neighbour_view = numpy.lib.stride_tricks.as_strided(
- points,
- shape=(dim0 - 1, dim1 - 1, 2, 2, points.shape[2]),
- strides=points.strides[:2] + points.strides[:2] + points.strides[-1:], writeable=False)
- inner_points = numpy.mean(neighbour_view, axis=(2, 3))
- grid_points[1:-1, 1:-1] = inner_points
-
- # Compute 'vertical' sides
- # Alternative: grid_points[1:-1, [0, -1]] = points[:-1, [0, -1]] + points[1:, [0, -1]] - inner_points[:, [0, -1]]
- grid_points[1:-1, [0, -1], 0] = points[:-1, [0, -1], 0] + points[1:, [0, -1], 0] - inner_points[:, [0, -1], 0]
- grid_points[1:-1, [0, -1], 1] = inner_points[:, [0, -1], 1]
-
- # Compute 'horizontal' sides
- grid_points[[0, -1], 1:-1, 0] = inner_points[[0, -1], :, 0]
- grid_points[[0, -1], 1:-1, 1] = points[[0, -1], :-1, 1] + points[[0, -1], 1:, 1] - inner_points[[0, -1], :, 1]
-
- # Compute corners
- d0, d1 = [0, 0, -1, -1], [0, -1, -1, 0]
- grid_points[d0, d1] = 2 * points[d0, d1] - inner_points[d0, d1]
- return grid_points
-
-
-def _quadrilateral_grid_as_triangles(points):
- """Returns the points and indices to make a grid of quadirlaterals
-
- :param numpy.ndarray points:
- 3D array of points (height, width, 2)
- :return: triangle corners (4 * N, 2), triangle indices (2 * N, 3)
- With N = height * width, the number of input points
- """
- nbpoints = numpy.prod(points.shape[:2])
-
- grid = _quadrilateral_grid_coords(points)
- coords = numpy.empty((4 * nbpoints, 2), dtype=grid.dtype)
- coords[::4] = grid[:-1, :-1].reshape(-1, 2)
- coords[1::4] = grid[1:, :-1].reshape(-1, 2)
- coords[2::4] = grid[:-1, 1:].reshape(-1, 2)
- coords[3::4] = grid[1:, 1:].reshape(-1, 2)
-
- indices = numpy.empty((2 * nbpoints, 3), dtype=numpy.uint32)
- indices[::2, 0] = numpy.arange(0, 4 * nbpoints, 4)
- indices[::2, 1] = numpy.arange(1, 4 * nbpoints, 4)
- indices[::2, 2] = numpy.arange(2, 4 * nbpoints, 4)
- indices[1::2, 0] = indices[::2, 1]
- indices[1::2, 1] = indices[::2, 2]
- indices[1::2, 2] = numpy.arange(3, 4 * nbpoints, 4)
-
- return coords, indices
-
-
-_RegularGridInfo = namedtuple(
- '_RegularGridInfo', ['bounds', 'origin', 'scale', 'shape', 'order'])
-
-
-_HistogramInfo = namedtuple(
- '_HistogramInfo', ['mean', 'count', 'sum', 'origin', 'scale', 'shape'])
-
-
-class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
- """Description of a scatter"""
-
- _DEFAULT_SELECTABLE = True
- """Default selectable state for scatter plots"""
-
- _SUPPORTED_SCATTER_VISUALIZATION = (
- ScatterVisualizationMixIn.Visualization.POINTS,
- ScatterVisualizationMixIn.Visualization.SOLID,
- ScatterVisualizationMixIn.Visualization.REGULAR_GRID,
- ScatterVisualizationMixIn.Visualization.IRREGULAR_GRID,
- ScatterVisualizationMixIn.Visualization.BINNED_STATISTIC,
- )
- """Overrides supported Visualizations"""
-
- def __init__(self):
- PointsBase.__init__(self)
- ColormapMixIn.__init__(self)
- ScatterVisualizationMixIn.__init__(self)
- self._value = ()
- self.__alpha = None
- # Cache Delaunay triangulation future object
- self.__delaunayFuture = None
- # Cache interpolator future object
- self.__interpolatorFuture = None
- self.__executor = None
-
- # Cache triangles: x, y, indices
- self.__cacheTriangles = None, None, None
-
- # Cache regular grid and histogram info
- self.__cacheRegularGridInfo = None
- self.__cacheHistogramInfo = None
-
- def _updateColormappedData(self):
- """Update the colormapped data, to be called when changed"""
- if self.getVisualization() is self.Visualization.BINNED_STATISTIC:
- histoInfo = self.__getHistogramInfo()
- if histoInfo is None:
- data = None
- else:
- data = getattr(
- histoInfo,
- self.getVisualizationParameter(
- self.VisualizationParameter.BINNED_STATISTIC_FUNCTION))
- else:
- data = self.getValueData(copy=False)
- self._setColormappedData(data, copy=False)
-
- @docstring(ScatterVisualizationMixIn)
- def setVisualization(self, mode):
- previous = self.getVisualization()
- if super().setVisualization(mode):
- if (bool(mode is self.Visualization.BINNED_STATISTIC) ^
- bool(previous is self.Visualization.BINNED_STATISTIC)):
- self._updateColormappedData()
- return True
- else:
- return False
-
- @docstring(ScatterVisualizationMixIn)
- def setVisualizationParameter(self, parameter, value):
- parameter = self.VisualizationParameter.from_value(parameter)
-
- if super(Scatter, self).setVisualizationParameter(parameter, value):
- if parameter in (self.VisualizationParameter.GRID_BOUNDS,
- self.VisualizationParameter.GRID_MAJOR_ORDER,
- self.VisualizationParameter.GRID_SHAPE):
- self.__cacheRegularGridInfo = None
-
- if parameter in (self.VisualizationParameter.BINNED_STATISTIC_SHAPE,
- self.VisualizationParameter.BINNED_STATISTIC_FUNCTION,
- self.VisualizationParameter.DATA_BOUNDS_HINT):
- if parameter in (self.VisualizationParameter.BINNED_STATISTIC_SHAPE,
- self.VisualizationParameter.DATA_BOUNDS_HINT):
- self.__cacheHistogramInfo = None # Clean-up cache
- if self.getVisualization() is self.Visualization.BINNED_STATISTIC:
- self._updateColormappedData()
- return True
- else:
- return False
-
- @docstring(ScatterVisualizationMixIn)
- def getCurrentVisualizationParameter(self, parameter):
- value = self.getVisualizationParameter(parameter)
- if (parameter is self.VisualizationParameter.DATA_BOUNDS_HINT or
- value is not None):
- return value # Value has been set, return it
-
- elif parameter is self.VisualizationParameter.GRID_BOUNDS:
- grid = self.__getRegularGridInfo()
- return None if grid is None else grid.bounds
-
- elif parameter is self.VisualizationParameter.GRID_MAJOR_ORDER:
- grid = self.__getRegularGridInfo()
- return None if grid is None else grid.order
-
- elif parameter is self.VisualizationParameter.GRID_SHAPE:
- grid = self.__getRegularGridInfo()
- return None if grid is None else grid.shape
-
- elif parameter is self.VisualizationParameter.BINNED_STATISTIC_SHAPE:
- info = self.__getHistogramInfo()
- return None if info is None else info.shape
-
- else:
- raise NotImplementedError()
-
- def __getRegularGridInfo(self):
- """Get grid info"""
- if self.__cacheRegularGridInfo is None:
- shape = self.getVisualizationParameter(
- self.VisualizationParameter.GRID_SHAPE)
- order = self.getVisualizationParameter(
- self.VisualizationParameter.GRID_MAJOR_ORDER)
- if shape is None or order is None:
- guess = _guess_grid(self.getXData(copy=False),
- self.getYData(copy=False))
- if guess is None:
- _logger.warning(
- 'Cannot guess a grid: Cannot display as regular grid image')
- return None
- if shape is None:
- shape = guess[1]
- if order is None:
- order = guess[0]
-
- nbpoints = len(self.getXData(copy=False))
- if nbpoints > shape[0] * shape[1]:
- # More data points that provided grid shape: enlarge grid
- _logger.warning(
- "More data points than provided grid shape size: extends grid")
- dim0, dim1 = shape
- if order == 'row': # keep dim1, enlarge dim0
- dim0 = nbpoints // dim1 + (1 if nbpoints % dim1 else 0)
- else: # keep dim0, enlarge dim1
- dim1 = nbpoints // dim0 + (1 if nbpoints % dim0 else 0)
- shape = dim0, dim1
-
- bounds = self.getVisualizationParameter(
- self.VisualizationParameter.GRID_BOUNDS)
- if bounds is None:
- x, y = self.getXData(copy=False), self.getYData(copy=False)
- min_, max_ = min_max(x)
- xRange = (min_, max_) if (x[0] - min_) < (max_ - x[0]) else (max_, min_)
- min_, max_ = min_max(y)
- yRange = (min_, max_) if (y[0] - min_) < (max_ - y[0]) else (max_, min_)
- bounds = (xRange[0], yRange[0]), (xRange[1], yRange[1])
-
- begin, end = bounds
- scale = ((end[0] - begin[0]) / max(1, shape[1] - 1),
- (end[1] - begin[1]) / max(1, shape[0] - 1))
- if scale[0] == 0 and scale[1] == 0:
- scale = 1., 1.
- elif scale[0] == 0:
- scale = scale[1], scale[1]
- elif scale[1] == 0:
- scale = scale[0], scale[0]
-
- origin = begin[0] - 0.5 * scale[0], begin[1] - 0.5 * scale[1]
-
- self.__cacheRegularGridInfo = _RegularGridInfo(
- bounds=bounds, origin=origin, scale=scale, shape=shape, order=order)
-
- return self.__cacheRegularGridInfo
-
- def __getHistogramInfo(self):
- """Get histogram info"""
- if self.__cacheHistogramInfo is None:
- shape = self.getVisualizationParameter(
- self.VisualizationParameter.BINNED_STATISTIC_SHAPE)
- if shape is None:
- shape = 100, 100 # TODO compute auto shape
-
- x, y, values = self.getData(copy=False)[:3]
- if len(x) == 0: # No histogram
- return None
-
- if not numpy.issubdtype(x.dtype, numpy.floating):
- x = x.astype(numpy.float64)
- if not numpy.issubdtype(y.dtype, numpy.floating):
- y = y.astype(numpy.float64)
- if not numpy.issubdtype(values.dtype, numpy.floating):
- values = values.astype(numpy.float64)
-
- ranges = (tuple(min_max(y, finite=True)),
- tuple(min_max(x, finite=True)))
- rangesHint = self.getVisualizationParameter(
- self.VisualizationParameter.DATA_BOUNDS_HINT)
- if rangesHint is not None:
- ranges = tuple((min(dataMin, hintMin), max(dataMax, hintMax))
- for (dataMin, dataMax), (hintMin, hintMax) in zip(ranges, rangesHint))
-
- points = numpy.transpose(numpy.array((y, x)))
- counts, sums, bin_edges = Histogramnd(
- points,
- histo_range=ranges,
- n_bins=shape,
- weights=values)
- yEdges, xEdges = bin_edges
- origin = xEdges[0], yEdges[0]
- scale = ((xEdges[-1] - xEdges[0]) / (len(xEdges) - 1),
- (yEdges[-1] - yEdges[0]) / (len(yEdges) - 1))
-
- with numpy.errstate(divide='ignore', invalid='ignore'):
- histo = sums / counts
-
- self.__cacheHistogramInfo = _HistogramInfo(
- mean=histo, count=counts, sum=sums,
- origin=origin, scale=scale, shape=shape)
-
- return self.__cacheHistogramInfo
-
- def _addBackendRenderer(self, backend):
- """Update backend renderer"""
- # Filter-out values <= 0
- xFiltered, yFiltered, valueFiltered, xerror, yerror = self.getData(
- copy=False, displayed=True)
-
- # Remove not finite numbers (this includes filtered out x, y <= 0)
- mask = numpy.logical_and(numpy.isfinite(xFiltered), numpy.isfinite(yFiltered))
- xFiltered = xFiltered[mask]
- yFiltered = yFiltered[mask]
-
- if len(xFiltered) == 0:
- return None # No data to display, do not add renderer to backend
-
- visualization = self.getVisualization()
-
- if visualization is self.Visualization.BINNED_STATISTIC:
- plot = self.getPlot()
- if (plot is None or
- plot.getXAxis().getScale() != Axis.LINEAR or
- plot.getYAxis().getScale() != Axis.LINEAR):
- # Those visualizations are not available with log scaled axes
- return None
-
- histoInfo = self.__getHistogramInfo()
- if histoInfo is None:
- return None
- data = getattr(histoInfo, self.getVisualizationParameter(
- self.VisualizationParameter.BINNED_STATISTIC_FUNCTION))
-
- return backend.addImage(
- data=data,
- origin=histoInfo.origin,
- scale=histoInfo.scale,
- colormap=self.getColormap(),
- alpha=self.getAlpha())
-
- # Compute colors
- cmap = self.getColormap()
- rgbacolors = cmap.applyToData(self)
-
- if self.__alpha is not None:
- rgbacolors[:, -1] = (rgbacolors[:, -1] * self.__alpha).astype(numpy.uint8)
-
- visualization = self.getVisualization()
-
- if visualization is self.Visualization.POINTS:
- return backend.addCurve(xFiltered, yFiltered,
- color=rgbacolors[mask],
- symbol=self.getSymbol(),
- linewidth=0,
- linestyle="",
- yaxis='left',
- xerror=xerror,
- yerror=yerror,
- fill=False,
- alpha=self.getAlpha(),
- symbolsize=self.getSymbolSize(),
- baseline=None)
-
- else:
- plot = self.getPlot()
- if (plot is None or
- plot.getXAxis().getScale() != Axis.LINEAR or
- plot.getYAxis().getScale() != Axis.LINEAR):
- # Those visualizations are not available with log scaled axes
- return None
-
- if visualization is self.Visualization.SOLID:
- triangulation = self._getDelaunay().result()
- if triangulation is None:
- _logger.warning(
- 'Cannot get a triangulation: Cannot display as solid surface')
- return None
- else:
- triangles = triangulation.simplices.astype(numpy.int32)
- return backend.addTriangles(xFiltered,
- yFiltered,
- triangles,
- color=rgbacolors[mask],
- alpha=self.getAlpha())
-
- elif visualization is self.Visualization.REGULAR_GRID:
- gridInfo = self.__getRegularGridInfo()
- if gridInfo is None:
- return None
-
- dim0, dim1 = gridInfo.shape
- if gridInfo.order == 'column': # transposition needed
- dim0, dim1 = dim1, dim0
-
- if len(rgbacolors) == dim0 * dim1:
- image = rgbacolors.reshape(dim0, dim1, -1)
- else:
- # The points do not fill the whole image
- image = numpy.empty((dim0 * dim1, 4), dtype=rgbacolors.dtype)
- image[:len(rgbacolors)] = rgbacolors
- image[len(rgbacolors):] = 0, 0, 0, 0 # Transparent pixels
- image.shape = dim0, dim1, -1
-
- if gridInfo.order == 'column':
- image = numpy.transpose(image, axes=(1, 0, 2))
-
- return backend.addImage(
- data=image,
- origin=gridInfo.origin,
- scale=gridInfo.scale,
- colormap=None,
- alpha=self.getAlpha())
-
- elif visualization is self.Visualization.IRREGULAR_GRID:
- gridInfo = self.__getRegularGridInfo()
- if gridInfo is None:
- return None
-
- shape = gridInfo.shape
- if shape is None: # No shape, no display
- return None
-
- nbpoints = len(xFiltered)
- if nbpoints == 1:
- # single point, render as a square points
- return backend.addCurve(xFiltered, yFiltered,
- color=rgbacolors[mask],
- symbol='s',
- linewidth=0,
- linestyle="",
- yaxis='left',
- xerror=None,
- yerror=None,
- fill=False,
- alpha=self.getAlpha(),
- symbolsize=7,
- baseline=None)
-
- # Make shape include all points
- gridOrder = gridInfo.order
- if nbpoints != numpy.prod(shape):
- if gridOrder == 'row':
- shape = int(numpy.ceil(nbpoints / shape[1])), shape[1]
- else: # column-major order
- shape = shape[0], int(numpy.ceil(nbpoints / shape[0]))
-
- if shape[0] < 2 or shape[1] < 2: # Single line, at least 2 points
- points = numpy.ones((2, nbpoints, 2), dtype=numpy.float64)
- # Use row/column major depending on shape, not on info value
- gridOrder = 'row' if shape[0] == 1 else 'column'
-
- if gridOrder == 'row':
- points[0, :, 0] = xFiltered
- points[0, :, 1] = yFiltered
- else: # column-major order
- points[0, :, 0] = yFiltered
- points[0, :, 1] = xFiltered
-
- # Add a second line that will be clipped in the end
- points[1, :-1] = points[0, :-1] + numpy.cross(
- points[0, 1:] - points[0, :-1], (0., 0., 1.))[:, :2]
- points[1, -1] = points[0, -1] + numpy.cross(
- points[0, -1] - points[0, -2], (0., 0., 1.))[:2]
-
- points.shape = 2, nbpoints, 2 # Use same shape for both orders
- coords, indices = _quadrilateral_grid_as_triangles(points)
-
- elif gridOrder == 'row': # row-major order
- if nbpoints != numpy.prod(shape):
- points = numpy.empty((numpy.prod(shape), 2), dtype=numpy.float64)
- points[:nbpoints, 0] = xFiltered
- points[:nbpoints, 1] = yFiltered
- # Index of last element of last fully filled row
- index = (nbpoints // shape[1]) * shape[1]
- points[nbpoints:, 0] = xFiltered[index - (numpy.prod(shape) - nbpoints):index]
- points[nbpoints:, 1] = yFiltered[-1]
- else:
- points = numpy.transpose((xFiltered, yFiltered))
- points.shape = shape[0], shape[1], 2
-
- else: # column-major order
- if nbpoints != numpy.prod(shape):
- points = numpy.empty((numpy.prod(shape), 2), dtype=numpy.float64)
- points[:nbpoints, 0] = yFiltered
- points[:nbpoints, 1] = xFiltered
- # Index of last element of last fully filled column
- index = (nbpoints // shape[0]) * shape[0]
- points[nbpoints:, 0] = yFiltered[index - (numpy.prod(shape) - nbpoints):index]
- points[nbpoints:, 1] = xFiltered[-1]
- else:
- points = numpy.transpose((yFiltered, xFiltered))
- points.shape = shape[1], shape[0], 2
-
- coords, indices = _quadrilateral_grid_as_triangles(points)
-
- # Remove unused extra triangles
- coords = coords[:4*nbpoints]
- indices = indices[:2*nbpoints]
-
- if gridOrder == 'row':
- x, y = coords[:, 0], coords[:, 1]
- else: # column-major order
- y, x = coords[:, 0], coords[:, 1]
-
- rgbacolors = rgbacolors[mask] # Filter-out not finite points
- gridcolors = numpy.empty(
- (4 * nbpoints, rgbacolors.shape[-1]), dtype=rgbacolors.dtype)
- for first in range(4):
- gridcolors[first::4] = rgbacolors[:nbpoints]
-
- return backend.addTriangles(x,
- y,
- indices,
- color=gridcolors,
- alpha=self.getAlpha())
-
- else:
- _logger.error("Unhandled visualization %s", visualization)
- return None
-
- @docstring(PointsBase)
- def pick(self, x, y):
- result = super(Scatter, self).pick(x, y)
-
- if result is not None:
- visualization = self.getVisualization()
-
- if visualization is self.Visualization.IRREGULAR_GRID:
- # Specific handling of picking for the irregular grid mode
- index = result.getIndices(copy=False)[0] // 4
- result = PickingResult(self, (index,))
-
- elif visualization is self.Visualization.REGULAR_GRID:
- # Specific handling of picking for the regular grid mode
- picked = result.getIndices(copy=False)
- if picked is None:
- return None
- row, column = picked[0][0], picked[1][0]
-
- gridInfo = self.__getRegularGridInfo()
- if gridInfo is None:
- return None
-
- if gridInfo.order == 'row':
- index = row * gridInfo.shape[1] + column
- else:
- index = row + column * gridInfo.shape[0]
- if index >= len(self.getXData(copy=False)): # OK as long as not log scale
- return None # Image can be larger than scatter
-
- result = PickingResult(self, (index,))
-
- elif visualization is self.Visualization.BINNED_STATISTIC:
- picked = result.getIndices(copy=False)
- if picked is None or len(picked) == 0 or len(picked[0]) == 0:
- return None
- row, col = picked[0][0], picked[1][0]
- histoInfo = self.__getHistogramInfo()
- if histoInfo is None:
- return None
- sx, sy = histoInfo.scale
- ox, oy = histoInfo.origin
- xdata = self.getXData(copy=False)
- ydata = self.getYData(copy=False)
- indices = numpy.nonzero(numpy.logical_and(
- numpy.logical_and(xdata >= ox + sx * col, xdata < ox + sx * (col + 1)),
- numpy.logical_and(ydata >= oy + sy * row, ydata < oy + sy * (row + 1))))[0]
- result = None if len(indices) == 0 else PickingResult(self, indices)
-
- return result
-
- def __getExecutor(self):
- """Returns async greedy executor
-
- :rtype: _GreedyThreadPoolExecutor
- """
- if self.__executor is None:
- self.__executor = _GreedyThreadPoolExecutor(max_workers=2)
- return self.__executor
-
- def _getDelaunay(self):
- """Returns a :class:`Future` which result is the Delaunay object.
-
- :rtype: concurrent.futures.Future
- """
- if self.__delaunayFuture is None or self.__delaunayFuture.cancelled():
- # Need to init a new delaunay
- x, y = self.getData(copy=False)[:2]
- # Remove not finite points
- mask = numpy.logical_and(numpy.isfinite(x), numpy.isfinite(y))
-
- self.__delaunayFuture = self.__getExecutor().submit_greedy(
- 'delaunay', delaunay, x[mask], y[mask])
-
- return self.__delaunayFuture
-
- @staticmethod
- def __initInterpolator(delaunayFuture, values):
- """Returns an interpolator for the given data points
-
- :param concurrent.futures.Future delaunayFuture:
- Future object which result is a Delaunay object
- :param numpy.ndarray values: The data value of valid points.
- :rtype: Union[callable,None]
- """
- # Wait for Delaunay to complete
- try:
- triangulation = delaunayFuture.result()
- except CancelledError:
- triangulation = None
-
- if triangulation is None:
- interpolator = None # Error case
- else:
- # Lazy-loading of interpolator
- try:
- from scipy.interpolate import LinearNDInterpolator
- except ImportError:
- LinearNDInterpolator = None
-
- if LinearNDInterpolator is not None:
- interpolator = LinearNDInterpolator(triangulation, values)
-
- # First call takes a while, do it here
- interpolator([(0., 0.)])
-
- else:
- # Fallback using matplotlib interpolator
- import matplotlib.tri
-
- x, y = triangulation.points.T
- tri = matplotlib.tri.Triangulation(
- x, y, triangles=triangulation.simplices)
- mplInterpolator = matplotlib.tri.LinearTriInterpolator(
- tri, values)
-
- # Wrap interpolator to have same API as scipy's one
- def interpolator(points):
- return mplInterpolator(*points.T)
-
- return interpolator
-
- def _getInterpolator(self):
- """Returns a :class:`Future` which result is the interpolator.
-
- The interpolator is a callable taking an array Nx2 of points
- as a single argument.
- The :class:`Future` result is None in case the interpolator cannot
- be initialized.
-
- :rtype: concurrent.futures.Future
- """
- if (self.__interpolatorFuture is None or
- self.__interpolatorFuture.cancelled()):
- # Need to init a new interpolator
- x, y, values = self.getData(copy=False)[:3]
- # Remove not finite points
- mask = numpy.logical_and(numpy.isfinite(x), numpy.isfinite(y))
- x, y, values = x[mask], y[mask], values[mask]
-
- self.__interpolatorFuture = self.__getExecutor().submit_greedy(
- 'interpolator',
- self.__initInterpolator, self._getDelaunay(), values)
- return self.__interpolatorFuture
-
- def _logFilterData(self, xPositive, yPositive):
- """Filter out values with x or y <= 0 on log axes
-
- :param bool xPositive: True to filter arrays according to X coords.
- :param bool yPositive: True to filter arrays according to Y coords.
- :return: The filtered arrays or unchanged object if not filtering needed
- :rtype: (x, y, value, xerror, yerror)
- """
- # overloaded from PointsBase to filter also value.
- value = self.getValueData(copy=False)
-
- if xPositive or yPositive:
- clipped = self._getClippingBoolArray(xPositive, yPositive)
-
- if numpy.any(clipped):
- # copy to keep original array and convert to float
- value = numpy.array(value, copy=True, dtype=numpy.float64)
- value[clipped] = numpy.nan
-
- x, y, xerror, yerror = PointsBase._logFilterData(self, xPositive, yPositive)
-
- return x, y, value, xerror, yerror
-
- def getValueData(self, copy=True):
- """Returns the value assigned to the scatter data points.
-
- :param copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: numpy.ndarray
- """
- return numpy.array(self._value, copy=copy)
-
- def getAlphaData(self, copy=True):
- """Returns the alpha (transparency) assigned to the scatter data points.
-
- :param copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: numpy.ndarray
- """
- return numpy.array(self.__alpha, copy=copy)
-
- def getData(self, copy=True, displayed=False):
- """Returns the x, y coordinates and the value of the data points
-
- :param copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :param bool displayed: True to only get curve points that are displayed
- in the plot. Default: False.
- Note: If plot has log scale, negative points
- are not displayed.
- :returns: (x, y, value, xerror, yerror)
- :rtype: 5-tuple of numpy.ndarray
- """
- if displayed:
- data = self._getCachedData()
- if data is not None:
- assert len(data) == 5
- return data
-
- return (self.getXData(copy),
- self.getYData(copy),
- self.getValueData(copy),
- self.getXErrorData(copy),
- self.getYErrorData(copy))
-
- # reimplemented from PointsBase to handle `value`
- def setData(self, x, y, value, xerror=None, yerror=None, alpha=None, copy=True):
- """Set the data of the scatter.
-
- :param numpy.ndarray x: The data corresponding to the x coordinates.
- :param numpy.ndarray y: The data corresponding to the y coordinates.
- :param numpy.ndarray value: The data corresponding to the value of
- the data points.
- :param xerror: Values with the uncertainties on the x values
- :type xerror: A float, or a numpy.ndarray of float32.
- If it is an array, it can either be a 1D array of
- same length as the data or a 2D array with 2 rows
- of same length as the data: row 0 for positive errors,
- row 1 for negative errors.
- :param yerror: Values with the uncertainties on the y values
- :type yerror: A float, or a numpy.ndarray of float32. See xerror.
- :param alpha: Values with the transparency (between 0 and 1)
- :type alpha: A float, or a numpy.ndarray of float32
- :param bool copy: True make a copy of the data (default),
- False to use provided arrays.
- """
- value = numpy.array(value, copy=copy)
- assert value.ndim == 1
- assert len(x) == len(value)
-
- # Convert complex data
- if numpy.iscomplexobj(value):
- _logger.warning(
- 'Converting value data to absolute value to plot it.')
- value = numpy.absolute(value)
-
- # Reset triangulation and interpolator
- if self.__delaunayFuture is not None:
- self.__delaunayFuture.cancel()
- self.__delaunayFuture = None
- if self.__interpolatorFuture is not None:
- self.__interpolatorFuture.cancel()
- self.__interpolatorFuture = None
-
- # Data changed, this needs update
- self.__cacheRegularGridInfo = None
- self.__cacheHistogramInfo = None
-
- self._value = value
- self._updateColormappedData()
-
- if alpha is not None:
- # Make sure alpha is an array of float in [0, 1]
- alpha = numpy.array(alpha, copy=copy)
- assert alpha.ndim == 1
- assert len(x) == len(alpha)
- if alpha.dtype.kind != 'f':
- alpha = alpha.astype(numpy.float32)
- if numpy.any(numpy.logical_or(alpha < 0., alpha > 1.)):
- alpha = numpy.clip(alpha, 0., 1.)
- self.__alpha = alpha
-
- # set x, y, xerror, yerror
-
- # call self._updated + plot._invalidateDataRange()
- PointsBase.setData(self, x, y, xerror, yerror, copy)
diff --git a/silx/gui/plot/items/shape.py b/silx/gui/plot/items/shape.py
deleted file mode 100644
index 955dfe3..0000000
--- a/silx/gui/plot/items/shape.py
+++ /dev/null
@@ -1,288 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""This module provides the :class:`Shape` item of the :class:`Plot`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "21/12/2018"
-
-
-import logging
-
-import numpy
-import six
-
-from ... import colors
-from .core import (
- Item, DataItem,
- ColorMixIn, FillMixIn, ItemChangedType, LineMixIn, YAxisMixIn)
-
-
-_logger = logging.getLogger(__name__)
-
-
-# TODO probably make one class for each kind of shape
-# TODO check fill:polygon/polyline + fill = duplicated
-class Shape(Item, ColorMixIn, FillMixIn, LineMixIn):
- """Description of a shape item
-
- :param str type_: The type of shape in:
- 'hline', 'polygon', 'rectangle', 'vline', 'polylines'
- """
-
- def __init__(self, type_):
- Item.__init__(self)
- ColorMixIn.__init__(self)
- FillMixIn.__init__(self)
- LineMixIn.__init__(self)
- self._overlay = False
- assert type_ in ('hline', 'polygon', 'rectangle', 'vline', 'polylines')
- self._type = type_
- self._points = ()
- self._lineBgColor = None
-
- self._handle = None
-
- def _addBackendRenderer(self, backend):
- """Update backend renderer"""
- points = self.getPoints(copy=False)
- x, y = points.T[0], points.T[1]
- return backend.addShape(x,
- y,
- shape=self.getType(),
- color=self.getColor(),
- fill=self.isFill(),
- overlay=self.isOverlay(),
- linestyle=self.getLineStyle(),
- linewidth=self.getLineWidth(),
- linebgcolor=self.getLineBgColor())
-
- def isOverlay(self):
- """Return true if shape is drawn as an overlay
-
- :rtype: bool
- """
- return self._overlay
-
- def setOverlay(self, overlay):
- """Set the overlay state of the shape
-
- :param bool overlay: True to make it an overlay
- """
- overlay = bool(overlay)
- if overlay != self._overlay:
- self._overlay = overlay
- self._updated(ItemChangedType.OVERLAY)
-
- def getType(self):
- """Returns the type of shape to draw.
-
- One of: 'hline', 'polygon', 'rectangle', 'vline', 'polylines'
-
- :rtype: str
- """
- return self._type
-
- def getPoints(self, copy=True):
- """Get the control points of the shape.
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :return: Array of point coordinates
- :rtype: numpy.ndarray with 2 dimensions
- """
- return numpy.array(self._points, copy=copy)
-
- def setPoints(self, points, copy=True):
- """Set the point coordinates
-
- :param numpy.ndarray points: Array of point coordinates
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :return:
- """
- self._points = numpy.array(points, copy=copy)
- self._updated(ItemChangedType.DATA)
-
- def getLineBgColor(self):
- """Returns the RGBA color of the item
- :rtype: 4-tuple of float in [0, 1] or array of colors
- """
- return self._lineBgColor
-
- def setLineBgColor(self, color, copy=True):
- """Set item color
- :param color: color(s) to be used
- :type color: str ("#RRGGBB") or (npoints, 4) unsigned byte array or
- one of the predefined color names defined in colors.py
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- """
- if color is not None:
- if isinstance(color, six.string_types):
- color = colors.rgba(color)
- else:
- color = numpy.array(color, copy=copy)
- # TODO more checks + improve color array support
- if color.ndim == 1: # Single RGBA color
- color = colors.rgba(color)
- else: # Array of colors
- assert color.ndim == 2
-
- self._lineBgColor = color
- self._updated(ItemChangedType.LINE_BG_COLOR)
-
-
-class BoundingRect(DataItem, YAxisMixIn):
- """An invisible shape which enforce the plot view to display the defined
- space on autoscale.
-
- This item do not display anything. But if the visible property is true,
- this bounding box is used by the plot, if not, the bounding box is
- ignored. That's the default behaviour for plot items.
-
- It can be applied on the "left" or "right" axes. Not both at the same time.
- """
-
- def __init__(self):
- DataItem.__init__(self)
- YAxisMixIn.__init__(self)
- self.__bounds = None
-
- def setBounds(self, rect):
- """Set the bounding box of this item in data coordinates
-
- :param Union[None,List[float]] rect: (xmin, xmax, ymin, ymax) or None
- """
- if rect is not None:
- rect = float(rect[0]), float(rect[1]), float(rect[2]), float(rect[3])
- assert rect[0] <= rect[1]
- assert rect[2] <= rect[3]
-
- if rect != self.__bounds:
- self.__bounds = rect
- self._boundsChanged()
- self._updated(ItemChangedType.DATA)
-
- def _getBounds(self):
- if self.__bounds is None:
- return None
- plot = self.getPlot()
- if plot is not None:
- xPositive = plot.getXAxis()._isLogarithmic()
- yPositive = plot.getYAxis()._isLogarithmic()
- if xPositive or yPositive:
- bounds = list(self.__bounds)
- if xPositive and bounds[1] <= 0:
- return None
- if xPositive and bounds[0] <= 0:
- bounds[0] = bounds[1]
- if yPositive and bounds[3] <= 0:
- return None
- if yPositive and bounds[2] <= 0:
- bounds[2] = bounds[3]
- return tuple(bounds)
-
- return self.__bounds
-
-
-class _BaseExtent(DataItem):
- """Base class for :class:`XAxisExtent` and :class:`YAxisExtent`.
-
- :param str axis: Either 'x' or 'y'.
- """
-
- def __init__(self, axis='x'):
- assert axis in ('x', 'y')
- DataItem.__init__(self)
- self.__axis = axis
- self.__range = 1., 100.
-
- def setRange(self, min_, max_):
- """Set the range of the extent of this item in data coordinates.
-
- :param float min_: Lower bound of the extent
- :param float max_: Upper bound of the extent
- :raises ValueError: If min > max or not finite bounds
- """
- range_ = float(min_), float(max_)
- if not numpy.all(numpy.isfinite(range_)):
- raise ValueError("min_ and max_ must be finite numbers.")
- if range_[0] > range_[1]:
- raise ValueError("min_ must be lesser or equal to max_")
-
- if range_ != self.__range:
- self.__range = range_
- self._boundsChanged()
- self._updated(ItemChangedType.DATA)
-
- def getRange(self):
- """Returns the range (min, max) of the extent in data coordinates.
-
- :rtype: List[float]
- """
- return self.__range
-
- def _getBounds(self):
- min_, max_ = self.getRange()
-
- plot = self.getPlot()
- if plot is not None:
- axis = plot.getXAxis() if self.__axis == 'x' else plot.getYAxis()
- if axis._isLogarithmic():
- if max_ <= 0:
- return None
- if min_ <= 0:
- min_ = max_
-
- if self.__axis == 'x':
- return min_, max_, float('nan'), float('nan')
- else:
- return float('nan'), float('nan'), min_, max_
-
-
-class XAxisExtent(_BaseExtent):
- """Invisible item with a settable horizontal data extent.
-
- This item do not display anything, but it behaves as a data
- item with a horizontal extent regarding plot data bounds, i.e.,
- :meth:`PlotWidget.resetZoom` will take this horizontal extent into account.
- """
- def __init__(self):
- _BaseExtent.__init__(self, axis='x')
-
-
-class YAxisExtent(_BaseExtent, YAxisMixIn):
- """Invisible item with a settable vertical data extent.
-
- This item do not display anything, but it behaves as a data
- item with a vertical extent regarding plot data bounds, i.e.,
- :meth:`PlotWidget.resetZoom` will take this vertical extent into account.
- """
-
- def __init__(self):
- _BaseExtent.__init__(self, axis='y')
- YAxisMixIn.__init__(self)