summaryrefslogtreecommitdiff
path: root/src/silx/gui/plot/items
diff options
context:
space:
mode:
Diffstat (limited to 'src/silx/gui/plot/items')
-rw-r--r--src/silx/gui/plot/items/__init__.py44
-rw-r--r--src/silx/gui/plot/items/_arc_roi.py256
-rw-r--r--src/silx/gui/plot/items/_band_roi.py18
-rw-r--r--src/silx/gui/plot/items/_roi_base.py168
-rw-r--r--src/silx/gui/plot/items/axis.py88
-rw-r--r--src/silx/gui/plot/items/complex.py65
-rw-r--r--src/silx/gui/plot/items/core.py409
-rw-r--r--src/silx/gui/plot/items/curve.py209
-rw-r--r--src/silx/gui/plot/items/histogram.py139
-rw-r--r--src/silx/gui/plot/items/image.py165
-rw-r--r--src/silx/gui/plot/items/image_aggregated.py30
-rwxr-xr-xsrc/silx/gui/plot/items/marker.py95
-rw-r--r--src/silx/gui/plot/items/roi.py320
-rw-r--r--src/silx/gui/plot/items/scatter.py464
-rw-r--r--src/silx/gui/plot/items/shape.py99
15 files changed, 1549 insertions, 1020 deletions
diff --git a/src/silx/gui/plot/items/__init__.py b/src/silx/gui/plot/items/__init__.py
index 6e26c64..bbb4220 100644
--- a/src/silx/gui/plot/items/__init__.py
+++ b/src/silx/gui/plot/items/__init__.py
@@ -31,22 +31,50 @@ __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 .core import (
+ Item,
+ DataItem, # noqa
+ LabelsMixIn,
+ DraggableMixIn,
+ ColormapMixIn,
+ LineGapColorMixIn, # 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, ImageDataBase, ImageRgba, ImageStack, MaskImageData # noqa
+from .image import (
+ ImageBase,
+ ImageData,
+ ImageDataBase,
+ ImageRgba,
+ ImageStack,
+ MaskImageData,
+) # noqa
from .image_aggregated import ImageDataAggregated # noqa
from .shape import Line, 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)
+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/src/silx/gui/plot/items/_arc_roi.py b/src/silx/gui/plot/items/_arc_roi.py
index 40711b7..658573a 100644
--- a/src/silx/gui/plot/items/_arc_roi.py
+++ b/src/silx/gui/plot/items/_arc_roi.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2018-2022 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2023 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
@@ -30,6 +30,8 @@ __date__ = "28/06/2018"
import logging
import numpy
+import enum
+from typing import Tuple
from ... import utils
from .. import items
@@ -50,8 +52,18 @@ class _ArcGeometry:
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):
+
+ 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
@@ -68,46 +80,59 @@ class _ArcGeometry:
@classmethod
def createEmpty(cls):
- """Create an arc geometry from an empty shape
- """
+ """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
- """
+ """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 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)
+ """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
+ 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)
+ 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
- """
+ """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
@@ -131,8 +156,7 @@ class _ArcGeometry:
)
def withEndAngle(self, endAngle):
- """Return a new geometry based on this object, with a specific end angle
- """
+ """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
@@ -161,9 +185,16 @@ class _ArcGeometry:
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)
+ return _ArcGeometry(
+ center,
+ startPoint,
+ endPoint,
+ self.radius,
+ self.weight,
+ self.startAngle,
+ self.endAngle,
+ self._closed,
+ )
def getKind(self):
"""Returns the kind of shape defined"""
@@ -191,14 +222,18 @@ class _ArcGeometry:
return self._closed
def __str__(self):
- return str((self.center,
- self.startPoint,
- self.endPoint,
- self.radius,
- self.weight,
- self.startAngle,
- self.endAngle,
- self._closed))
+ return str(
+ (
+ self.center,
+ self.startPoint,
+ self.endPoint,
+ self.radius,
+ self.weight,
+ self.startAngle,
+ self.endAngle,
+ self._closed,
+ )
+ )
class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
@@ -210,19 +245,37 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
- 1 anchor to translate the shape.
"""
- ICON = 'add-shape-arc'
- NAME = 'arc ROI'
+ 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")
+ 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")
+ MoveMode = RoiInteractionMode(
+ "Translation", "Provides anchors to only move the ROI"
+ )
+
+ class Role(enum.Enum):
+ """Identify a set of roles which can be used for now to reach some positions"""
+
+ START = 0
+ """Location of the anchor at the start of the arc"""
+ STOP = 1
+ """Location of the anchor at the stop of the arc"""
+ MIDDLE = 2
+ """Location of the anchor at the middle of the arc"""
+ CENTER = 3
+ """Location of the center of the circle"""
def __init__(self, parent=None):
HandleBasedROI.__init__(self, parent=parent)
@@ -265,22 +318,28 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
:param RoiInteractionMode modeId:
"""
if modeId is self.ThreePointMode:
+ self._handleStart.setVisible(True)
+ self._handleEnd.setVisible(True)
+ self._handleWeight.setVisible(True)
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.setVisible(True)
+ self._handleEnd.setVisible(True)
+ self._handleWeight.setVisible(True)
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._handleStart.setVisible(False)
+ self._handleEnd.setVisible(False)
+ self._handleWeight.setVisible(False)
self._handleMid.setSymbol("+")
- self._handleEnd.setSymbol("")
- self._handleWeight.setSymbol("")
self._handleMove.setSymbol("+")
else:
assert False
@@ -302,7 +361,7 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
self.__shape.setLineWidth(style.getLineWidth())
def setFirstShapePoints(self, points):
- """"Initialize the ROI using the points from the first interaction.
+ """Initialize the ROI using the points from the first interaction.
This interaction is constrained by the plot API and only supports few
shapes.
@@ -367,7 +426,9 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
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
+ weightPos = (
+ geometry.center + (geometry.radius + geometry.weight * 0.5) * vector
+ )
with utils.blockSignals(self._handleWeight):
self._handleWeight.setPosition(*weightPos)
@@ -393,7 +454,9 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
self._updateWeightHandle()
self._updateShape()
- def _updateCurvature(self, start, mid, end, updateCurveHandles, checkClosed=False, updateStart=False):
+ 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
@@ -418,7 +481,9 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
self._handleEnd.setPosition(*end)
weight = self._geometry.weight
- geometry = self._createGeometryFromControlPoints(start, mid, end, weight, closed=closed)
+ geometry = self._createGeometryFromControlPoints(
+ start, mid, end, weight, closed=closed
+ )
self._geometry = geometry
self._updateWeightHandle()
@@ -433,10 +498,10 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
sign = 1 if geometry.startAngle < geometry.endAngle else -1
if updateStart:
geometry.startPoint = geometry.endPoint
- geometry.startAngle = geometry.endAngle - sign * 2*numpy.pi
+ geometry.startAngle = geometry.endAngle - sign * 2 * numpy.pi
else:
geometry.endPoint = geometry.startPoint
- geometry.endAngle = geometry.startAngle + sign * 2*numpy.pi
+ geometry.endAngle = geometry.startAngle + sign * 2 * numpy.pi
def handleDragUpdated(self, handle, origin, previous, current):
modeId = self.getInteractionMode()
@@ -445,8 +510,12 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
mid = numpy.array(self._handleMid.getPosition())
end = numpy.array(self._handleEnd.getPosition())
self._updateCurvature(
- current, mid, end, checkClosed=True, updateStart=True,
- updateCurveHandles=False
+ current,
+ mid,
+ end,
+ checkClosed=True,
+ updateStart=True,
+ updateCurveHandles=False,
)
elif modeId is self.PolarMode:
v = current - self._geometry.center
@@ -477,8 +546,12 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
start = numpy.array(self._handleStart.getPosition())
mid = numpy.array(self._handleMid.getPosition())
self._updateCurvature(
- start, mid, current, checkClosed=True, updateStart=False,
- updateCurveHandles=False
+ start,
+ mid,
+ current,
+ checkClosed=True,
+ updateStart=False,
+ updateCurveHandles=False,
)
elif modeId is self.PolarMode:
v = current - self._geometry.center
@@ -511,8 +584,7 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
return abs(point1[0] - point2[0]) + abs(point1[1] - point2[1]) < 15
def _normalizeGeometry(self):
- """Keep the same phisical geometry, but with normalized parameters.
- """
+ """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
@@ -582,8 +654,9 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
if endAngle > startAngle:
endAngle -= 2 * numpy.pi
- return _ArcGeometry(center, start, end,
- radius, weight, startAngle, endAngle)
+ return _ArcGeometry(
+ center, start, end, radius, weight, startAngle, endAngle
+ )
def _createShapeFromGeometry(self, geometry):
kind = geometry.getKind()
@@ -595,11 +668,14 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
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])
+ 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
@@ -712,7 +788,29 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
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
+ return (
+ geometry.center,
+ self.getInnerRadius(),
+ self.getOuterRadius(),
+ geometry.startAngle,
+ geometry.endAngle,
+ )
+
+ def getPosition(self, role: Role = Role.CENTER) -> Tuple[float, float]:
+ """Returns a position by it's role.
+
+ By default returns the center of the circle of the arc ROI.
+ """
+ if role == self.Role.START:
+ return self._handleStart.getPosition()
+ if role == self.Role.STOP:
+ return self._handleEnd.getPosition()
+ if role == self.Role.MIDDLE:
+ return self._handleMid.getPosition()
+ if role == self.Role.CENTER:
+ p = self.getCenter()
+ return p[0], p[1]
+ raise ValueError(f"{role} is not supported")
def isClosed(self):
"""Returns true if the arc is a closed shape, like a circle or a donut.
@@ -795,9 +893,16 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
vector = numpy.array([numpy.cos(endAngle), numpy.sin(endAngle)])
endPoint = center + vector * radius
- geometry = _ArcGeometry(center, startPoint, endPoint,
- radius, weight,
- startAngle, endAngle, closed=None)
+ geometry = _ArcGeometry(
+ center,
+ startPoint,
+ endPoint,
+ radius,
+ weight,
+ startAngle,
+ endAngle,
+ closed=None,
+ )
self._geometry = geometry
self._updateHandles()
@@ -805,7 +910,9 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
def contains(self, position):
# first check distance, fastest
center = self.getCenter()
- distance = numpy.sqrt((position[1] - center[1]) ** 2 + ((position[0] - center[0])) ** 2)
+ 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
@@ -871,8 +978,15 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
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
+ 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/src/silx/gui/plot/items/_band_roi.py b/src/silx/gui/plot/items/_band_roi.py
index a60a177..0d2ad4e 100644
--- a/src/silx/gui/plot/items/_band_roi.py
+++ b/src/silx/gui/plot/items/_band_roi.py
@@ -100,7 +100,7 @@ class BandGeometry(NamedTuple):
def slope(self) -> float:
"""Slope of the line (begin, end), infinity for a vertical line"""
if self.begin.x == self.end.x:
- return float('inf')
+ return float("inf")
return (self.end.y - self.begin.y) / (self.end.x - self.begin.x)
@property
@@ -309,18 +309,20 @@ class BandROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
)
@staticmethod
- def __snap(point: Tuple[float, float], fixed: Tuple[float, float]) -> Tuple[float, float]:
+ def __snap(
+ point: Tuple[float, float], fixed: Tuple[float, float]
+ ) -> Tuple[float, float]:
"""Snap point so that vector [point, fixed] snap to direction 0, 45 or 90 degrees
:return: the snapped point position.
"""
vector = point[0] - fixed[0], point[1] - fixed[1]
angle = numpy.arctan2(vector[1], vector[0])
- snapAngle = numpy.pi/4 * numpy.round(angle / (numpy.pi/4))
+ snapAngle = numpy.pi / 4 * numpy.round(angle / (numpy.pi / 4))
length = numpy.linalg.norm(vector)
return (
fixed[0] + length * numpy.cos(snapAngle),
- fixed[1] + length * numpy.sin(snapAngle)
+ fixed[1] + length * numpy.sin(snapAngle),
)
def handleDragUpdated(self, handle, origin, previous, current):
@@ -353,12 +355,16 @@ class BandROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
def __handleWidthUpConstraint(self, x: float, y: float) -> Tuple[float, float]:
geometry = self.getGeometry()
- offset = max(0, numpy.dot(geometry.normal, numpy.array((x, y)) - geometry.center))
+ offset = max(
+ 0, numpy.dot(geometry.normal, numpy.array((x, y)) - geometry.center)
+ )
return tuple(geometry.center + offset * numpy.array(geometry.normal))
def __handleWidthDownConstraint(self, x: float, y: float) -> Tuple[float, float]:
geometry = self.getGeometry()
- offset = max(0, -numpy.dot(geometry.normal, numpy.array((x, y)) - geometry.center))
+ offset = max(
+ 0, -numpy.dot(geometry.normal, numpy.array((x, y)) - geometry.center)
+ )
return tuple(geometry.center - offset * numpy.array(geometry.normal))
@docstring(_RegionOfInterestBase)
diff --git a/src/silx/gui/plot/items/_roi_base.py b/src/silx/gui/plot/items/_roi_base.py
index 765a538..43c5381 100644
--- a/src/silx/gui/plot/items/_roi_base.py
+++ b/src/silx/gui/plot/items/_roi_base.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2023 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
@@ -37,14 +37,14 @@ __date__ = "28/06/2018"
import logging
import numpy
import weakref
+import functools
+from typing import Optional
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__)
@@ -68,8 +68,10 @@ class _RegionOfInterestBase(qt.QObject):
"""
def __init__(self, parent=None):
- qt.QObject.__init__(self, parent=parent)
- self.__name = ''
+ qt.QObject.__init__(self)
+ if parent is not None:
+ self.setParent(parent)
+ self.__name = ""
def getName(self):
"""Returns the name of the ROI
@@ -120,10 +122,12 @@ class RoiInteractionMode(object):
@property
def label(self):
+ """Short name"""
return self._label
@property
def description(self):
+ """Longer description of the interaction mode"""
return self._description
@@ -188,6 +192,28 @@ class InteractionModeMixIn(object):
"""
return self.__modeId
+ def createMenuForInteractionMode(self, parent: qt.QWidget) -> qt.QMenu:
+ """Create a menu providing access to the different interaction modes"""
+ availableModes = self.availableInteractionModes()
+ currentMode = self.getInteractionMode()
+ submenu = qt.QMenu(parent)
+ modeGroup = qt.QActionGroup(parent)
+ modeGroup.setExclusive(True)
+ for mode in availableModes:
+ action = qt.QAction(parent)
+ action.setText(mode.label)
+ action.setToolTip(mode.description)
+ action.setCheckable(True)
+ if mode is currentMode:
+ action.setChecked(True)
+ else:
+ callback = functools.partial(self.setInteractionMode, mode)
+ action.triggered.connect(callback)
+ modeGroup.addAction(action)
+ submenu.addAction(action)
+ submenu.setTitle("Interaction mode")
+ return submenu
+
class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
"""Object describing a region of interest in a plot.
@@ -196,10 +222,10 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
The RegionOfInterestManager that created this object
"""
- _DEFAULT_LINEWIDTH = 1.
+ _DEFAULT_LINEWIDTH = 1.0
"""Default line width of the curve"""
- _DEFAULT_LINESTYLE = '-'
+ _DEFAULT_LINESTYLE = "-"
"""Default line style of the curve"""
_DEFAULT_HIGHLIGHT_STYLE = items.CurveStyle(linewidth=2)
@@ -225,15 +251,18 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
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)
+ # Must be done before _RegionOfInterestBase.__init__
+ self._child = WeakList()
_RegionOfInterestBase.__init__(self, parent)
core.HighlightedMixIn.__init__(self)
- self._color = rgba('red')
+ self.__text = None
+ 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"""
@@ -263,8 +292,11 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
"""
# 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')
+
+ if parent is not None and not isinstance(
+ parent, roi_tools.RegionOfInterestManager
+ ):
+ raise ValueError("Unsupported parent")
previousParent = self.parent()
if previousParent is not None:
@@ -292,7 +324,7 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
"""
assert item is not None
self._child.append(item)
- if item.getName() == '':
+ if item.getName() == "":
self._setItemName(item)
manager = self.parent()
if manager is not None:
@@ -352,26 +384,6 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
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.
@@ -457,6 +469,26 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
self._visible = visible
self._updated(items.ItemChangedType.VISIBLE)
+ def getText(self) -> str:
+ """Returns the currently displayed text for this ROI"""
+ return self.getName() if self.__text is None else self.__text
+
+ def setText(self, text: Optional[str] = None) -> None:
+ """Set the displayed text for this ROI.
+
+ If None (the default), the ROI name is used.
+ """
+ if self.__text != text:
+ self.__text = text
+ self._updated(items.ItemChangedType.TEXT)
+
+ def _updateText(self, text: str) -> None:
+ """Update the text displayed by this ROI
+
+ Override in subclass to custom text display
+ """
+ pass
+
@classmethod
def showFirstInteractionShape(cls):
"""Returns True if the shape created by the first interaction and
@@ -478,7 +510,7 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
return cls._plotShape
def setFirstShapePoints(self, points):
- """"Initialize the ROI using the points from the first interaction.
+ """Initialize the ROI using the points from the first interaction.
This interaction is constrained by the plot API and only supports few
shapes.
@@ -486,13 +518,11 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
raise NotImplementedError()
def creationStarted(self):
- """"Called when the ROI creation interaction was started.
- """
+ """Called when the ROI creation interaction was started."""
pass
def creationFinalized(self):
- """"Called when the ROI creation interaction was finalized.
- """
+ """Called when the ROI creation interaction was finalized."""
pass
def _updateItemProperty(self, event, source, destination):
@@ -544,15 +574,23 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
assert False
def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.HIGHLIGHTED:
+ if event == items.ItemChangedType.TEXT:
+ self._updateText(self.getText())
+ elif event == items.ItemChangedType.HIGHLIGHTED:
+ for item in self.getItems():
+ zoffset = 1000 if self.isHighlighted() else 0
+ item.setZValue(item._DEFAULT_Z_LAYER + zoffset)
+
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]
+ 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)
@@ -562,7 +600,11 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
super(RegionOfInterest, self)._updated(event, checkVisibility)
- def _updatedStyle(self, event, style):
+ # Displayed text has changed, send a text event
+ if event == items.ItemChangedType.NAME and self.__text is None:
+ self._updated(items.ItemChangedType.TEXT, checkVisibility)
+
+ def _updatedStyle(self, event, style: items.CurveStyle):
"""Called when the current displayed style of the ROI was changed.
:param event: The event responsible of the change of the style
@@ -570,7 +612,7 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
"""
pass
- def getCurrentStyle(self):
+ def getCurrentStyle(self) -> items.CurveStyle:
"""Returns the current curve style.
Curve style depends on curve highlighting
@@ -588,7 +630,7 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
baseSymbol = self.getSymbol()
baseSymbolsize = self.getSymbolSize()
else:
- baseSymbol = 'o'
+ baseSymbol = "o"
baseSymbolsize = 1
if self.isHighlighted():
@@ -604,13 +646,16 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
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)
+ symbolsize=baseSymbolsize if symbolsize is None else symbolsize,
+ )
else:
- return items.CurveStyle(color=baseColor,
- linestyle=baseLinestyle,
- linewidth=baseLinewidth,
- symbol=baseSymbol,
- symbolsize=baseSymbolsize)
+ return items.CurveStyle(
+ color=baseColor,
+ linestyle=baseLinestyle,
+ linewidth=baseLinewidth,
+ symbol=baseSymbol,
+ symbolsize=baseSymbolsize,
+ )
def _editingStarted(self):
assert self._editable is True
@@ -619,6 +664,10 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
def _editingFinished(self):
self.sigEditingFinished.emit()
+ def populateContextMenu(self, menu: qt.QMenu):
+ """Populate a menu used as a context menu"""
+ pass
+
class HandleBasedROI(RegionOfInterest):
"""Manage a ROI based on a set of handles"""
@@ -730,9 +779,7 @@ class HandleBasedROI(RegionOfInterest):
See :class:`~silx.gui.plot.items.Item._updated`
"""
- if event == items.ItemChangedType.NAME:
- self._updateText(self.getName())
- elif event == items.ItemChangedType.VISIBLE:
+ if event == items.ItemChangedType.VISIBLE:
for item, role in self._handles:
visible = self.isVisible()
editionVisible = visible and self.isEditable()
@@ -754,9 +801,9 @@ class HandleBasedROI(RegionOfInterest):
color = rgba(self.getColor())
handleColor = self._computeHandleColor(color)
for item, role in self._handles:
- if role == 'user':
+ if role == "user":
pass
- elif role == 'label':
+ elif role == "label":
item.setColor(color)
else:
item.setColor(handleColor)
@@ -825,10 +872,3 @@ class HandleBasedROI(RegionOfInterest):
: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/src/silx/gui/plot/items/axis.py b/src/silx/gui/plot/items/axis.py
index fa3f6d7..1ae1ef1 100644
--- a/src/silx/gui/plot/items/axis.py
+++ b/src/silx/gui/plot/items/axis.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2022 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -24,28 +24,28 @@
"""This module provides the class for axes of the :class:`PlotWidget`.
"""
+from __future__ import annotations
+
__authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "22/11/2018"
import datetime as dt
import enum
-import logging
+from typing import Optional
import dateutil.tz
-import numpy
+from ....utils.proxy import docstring
from ... import qt
from .. import _utils
-_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
+
+ DEFAULT = 0 # Ticks are regular numbers
+ TIME_SERIES = 1 # Ticks are datetime objects
class Axis(qt.QObject):
@@ -53,6 +53,7 @@ class Axis(qt.QObject):
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.
@@ -91,10 +92,10 @@ class Axis(qt.QObject):
self._scale = self.LINEAR
self._isAutoScale = True
# Store default labels provided to setGraph[X|Y]Label
- self._defaultLabel = ''
+ self._defaultLabel = ""
# Store currently displayed labels
# Current label can differ from input one with active curve handling
- self._currentLabel = ''
+ self._currentLabel = ""
def _getPlot(self):
"""Returns the PlotWidget this Axis belongs to.
@@ -150,7 +151,12 @@ class Axis(qt.QObject):
:rtype: 2-tuple of float
"""
return _utils.checkAxisLimits(
- vmin, vmax, isLog=self._isLogarithmic(), name=self._defaultLabel)
+ vmin, vmax, isLog=self._isLogarithmic(), name=self._defaultLabel
+ )
+
+ def _getDataRange(self) -> Optional[tuple[float, float]]:
+ """Returns the range of data items over this axis as (vmin, vmax)"""
+ raise NotImplementedError()
def isInverted(self):
"""Return True if the axis is inverted (top to bottom for the y-axis),
@@ -172,6 +178,10 @@ class Axis(qt.QObject):
return
raise NotImplementedError()
+ def isVisible(self) -> bool:
+ """Returns whether the axis is displayed or not"""
+ return True
+
def getLabel(self):
"""Return the current displayed label of this axis.
@@ -199,10 +209,10 @@ class Axis(qt.QObject):
:param str label: Currently displayed label
"""
- if label is None or label == '':
+ if label is None or label == "":
label = self._defaultLabel
if label is None:
- label = ''
+ label = ""
self._currentLabel = label
self._internalSetCurrentLabel(label)
@@ -218,7 +228,7 @@ class Axis(qt.QObject):
:param str scale: Name of the scale ("log", or "linear")
"""
- assert(scale in self._SCALES)
+ assert scale in self._SCALES
if self._scale == scale:
return
@@ -227,6 +237,8 @@ class Axis(qt.QObject):
self._scale = scale
+ vmin, vmax = self.getLimits()
+
# TODO hackish way of forcing update of curves and images
plot = self._getPlot()
for item in plot.getItems():
@@ -235,13 +247,20 @@ class Axis(qt.QObject):
if scale == self.LOGARITHMIC:
self._internalSetLogarithmic(True)
+ if vmin <= 0:
+ dataRange = self._getDataRange()
+ if dataRange is None:
+ self.setLimits(1.0, 100.0)
+ else:
+ if vmax > 0 and dataRange[0] < vmax:
+ self.setLimits(dataRange[0], vmax)
+ else:
+ self.setLimits(*dataRange)
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)
@@ -328,7 +347,7 @@ class Axis(qt.QObject):
plot = self._getPlot()
xMin, xMax = plot.getXAxis().getLimits()
yMin, yMax = plot.getYAxis().getLimits()
- y2Min, y2Max = plot.getYAxis('right').getLimits()
+ y2Min, y2Max = plot.getYAxis("right").getLimits()
plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
return updated
@@ -351,7 +370,7 @@ class Axis(qt.QObject):
plot = self._getPlot()
xMin, xMax = plot.getXAxis().getLimits()
yMin, yMax = plot.getYAxis().getLimits()
- y2Min, y2Max = plot.getYAxis('right').getLimits()
+ y2Min, y2Max = plot.getYAxis("right").getLimits()
plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
return updated
@@ -368,7 +387,7 @@ class XAxis(Axis):
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)):
+ 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)
@@ -410,6 +429,11 @@ class XAxis(Axis):
updated = constrains.update(minXRange=minRange, maxXRange=maxRange)
return updated
+ @docstring(Axis)
+ def _getDataRange(self) -> Optional[tuple[float, float]]:
+ ranges = self._getPlot().getDataRange()
+ return ranges.x
+
class YAxis(Axis):
"""Axis class defining primitives for the Y axis"""
@@ -418,13 +442,13 @@ class YAxis(Axis):
# specialised implementations (prefixel by '_internal')
def _internalSetCurrentLabel(self, label):
- self._getBackend().setGraphYLabel(label, axis='left')
+ self._getBackend().setGraphYLabel(label, axis="left")
def _internalGetLimits(self):
- return self._getBackend().getGraphYLimits(axis='left')
+ return self._getBackend().getGraphYLimits(axis="left")
def _internalSetLimits(self, ymin, ymax):
- self._getBackend().setGraphYLimits(ymin, ymax, axis='left')
+ self._getBackend().setGraphYLimits(ymin, ymax, axis="left")
def _internalSetLogarithmic(self, flag):
self._getBackend().setYAxisLogarithmic(flag)
@@ -462,6 +486,11 @@ class YAxis(Axis):
updated = constrains.update(minYRange=minRange, maxYRange=maxRange)
return updated
+ @docstring(Axis)
+ def _getDataRange(self) -> Optional[tuple[float, float]]:
+ ranges = self._getPlot().getDataRange()
+ return ranges.y
+
class YRightAxis(Axis):
"""Proxy axis for the secondary Y axes. It manages it own label and limit
@@ -485,13 +514,13 @@ class YRightAxis(Axis):
self.__mainAxis.sigAutoScaleChanged.connect(self.sigAutoScaleChanged.emit)
def _internalSetCurrentLabel(self, label):
- self._getBackend().setGraphYLabel(label, axis='right')
+ self._getBackend().setGraphYLabel(label, axis="right")
def _internalGetLimits(self):
- return self._getBackend().getGraphYLimits(axis='right')
+ return self._getBackend().getGraphYLimits(axis="right")
def _internalSetLimits(self, ymin, ymax):
- self._getBackend().setGraphYLimits(ymin, ymax, axis='right')
+ self._getBackend().setGraphYLimits(ymin, ymax, axis="right")
def setInverted(self, flag=True):
"""Set the Y axis orientation.
@@ -505,6 +534,10 @@ class YRightAxis(Axis):
"""Return True if Y axis goes from top to bottom, False otherwise."""
return self.__mainAxis.isInverted()
+ def isVisible(self) -> bool:
+ """Returns whether the axis is displayed or not"""
+ return self._getBackend().isYRightAxisVisible()
+
def getScale(self):
"""Return the name of the scale used by this axis.
@@ -541,3 +574,8 @@ class YRightAxis(Axis):
False to disable it.
"""
return self.__mainAxis.setAutoScale(flag)
+
+ @docstring(Axis)
+ def _getDataRange(self) -> Optional[tuple[float, float]]:
+ ranges = self._getPlot().getDataRange()
+ return ranges.y2
diff --git a/src/silx/gui/plot/items/complex.py b/src/silx/gui/plot/items/complex.py
index 82d821f..d10767f 100644
--- a/src/silx/gui/plot/items/complex.py
+++ b/src/silx/gui/plot/items/complex.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -34,7 +34,6 @@ 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
@@ -45,6 +44,7 @@ _logger = logging.getLogger(__name__)
# Complex colormap functions
+
def _phase2rgb(colormap, data):
"""Creates RGBA image with colour-coded phase.
@@ -60,7 +60,7 @@ def _phase2rgb(colormap, data):
return colormap.applyToData(phase)
-def _complex2rgbalog(phaseColormap, data, amin=0., dlogs=2, smax=None):
+def _complex2rgbalog(phaseColormap, data, amin=0.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
@@ -117,7 +117,8 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
ComplexMixIn.ComplexMode.IMAGINARY,
ComplexMixIn.ComplexMode.AMPLITUDE_PHASE,
ComplexMixIn.ComplexMode.LOG10_AMPLITUDE_PHASE,
- ComplexMixIn.ComplexMode.SQUARE_AMPLITUDE)
+ ComplexMixIn.ComplexMode.SQUARE_AMPLITUDE,
+ )
"""Overrides supported ComplexMode"""
def __init__(self):
@@ -130,10 +131,7 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
# Use default from ColormapMixIn
colormap = super(ImageComplexData, self).getColormap()
- phaseColormap = Colormap(
- name='hsv',
- vmin=-numpy.pi,
- vmax=numpy.pi)
+ phaseColormap = Colormap(name="hsv", vmin=-numpy.pi, vmax=numpy.pi)
self._colormaps = { # Default colormaps for all modes
self.ComplexMode.ABSOLUTE: colormap,
@@ -154,8 +152,10 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
return None
mode = self.getComplexMode()
- if mode in (self.ComplexMode.AMPLITUDE_PHASE,
- self.ComplexMode.LOG10_AMPLITUDE_PHASE):
+ 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)
@@ -171,11 +171,13 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
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())
+ return backend.addImage(
+ data,
+ origin=self.getOrigin(),
+ scale=self.getScale(),
+ colormap=colormap,
+ alpha=self.getAlpha(),
+ )
@docstring(ComplexMixIn)
def setComplexMode(self, mode):
@@ -247,7 +249,7 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
return self._colormaps[mode]
def setData(self, data, copy=True):
- """"Set the image complex data
+ """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,
@@ -257,7 +259,8 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
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.')
+ "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
@@ -274,8 +277,9 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
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):
+ 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:
@@ -308,16 +312,18 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
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):
+ 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))
+ "Unsupported conversion mode: %s, fallback to absolute", str(mode)
+ )
return numpy.absolute(data)
def getData(self, copy=True, mode=None):
@@ -340,7 +346,8 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
if mode not in self._dataByModesCache:
self._dataByModesCache[mode] = self.__convertComplexData(
- self.getComplexData(copy=False), mode)
+ self.getComplexData(copy=False), mode
+ )
return numpy.array(self._dataByModesCache[mode], copy=copy)
@@ -373,11 +380,3 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
# 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/src/silx/gui/plot/items/core.py b/src/silx/gui/plot/items/core.py
index 074c168..7d754a7 100644
--- a/src/silx/gui/plot/items/core.py
+++ b/src/silx/gui/plot/items/core.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2022 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -23,16 +23,14 @@
# ###########################################################################*/
"""This module provides the base class for items of the :class:`Plot`.
"""
+from __future__ import annotations
+
__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 collections import abc
from copy import deepcopy
import logging
import enum
@@ -41,13 +39,12 @@ import weakref
import numpy
-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 ...colors import Colormap, _Colormappable
from ._pick import PickingResult
from silx import config
@@ -58,98 +55,109 @@ _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'
+ VISIBLE = "visibleChanged"
"""Item's visibility changed flag."""
- ZVALUE = 'zValueChanged'
+ ZVALUE = "zValueChanged"
"""Item's Z value changed flag."""
- COLORMAP = 'colormapChanged' # Emitted when set + forward events from the colormap object
+ 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'
+ SYMBOL = "symbolChanged"
"""Item's symbol changed flag."""
- SYMBOL_SIZE = 'symbolSizeChanged'
+ SYMBOL_SIZE = "symbolSizeChanged"
"""Item's symbol size changed flag."""
- LINE_WIDTH = 'lineWidthChanged'
+ LINE_WIDTH = "lineWidthChanged"
"""Item's line width changed flag."""
- LINE_STYLE = 'lineStyleChanged'
+ LINE_STYLE = "lineStyleChanged"
"""Item's line style changed flag."""
- COLOR = 'colorChanged'
+ COLOR = "colorChanged"
"""Item's color changed flag."""
- LINE_BG_COLOR = 'lineBgColorChanged'
- """Item's line background color changed flag."""
+ LINE_BG_COLOR = "lineBgColorChanged" # Deprecated, use LINE_GAP_COLOR
+
+ LINE_GAP_COLOR = "lineGapColorChanged"
+ """Item's dashed line gap color changed flag."""
- YAXIS = 'yAxisChanged'
+ YAXIS = "yAxisChanged"
"""Item's Y axis binding changed flag."""
- FILL = 'fillChanged'
+ FILL = "fillChanged"
"""Item's fill changed flag."""
- ALPHA = 'alphaChanged'
+ ALPHA = "alphaChanged"
"""Item's transparency alpha changed flag."""
- DATA = 'dataChanged'
+ DATA = "dataChanged"
"""Item's data changed flag"""
- MASK = 'maskChanged'
+ MASK = "maskChanged"
"""Item's mask changed flag"""
- HIGHLIGHTED = 'highlightedChanged'
+ HIGHLIGHTED = "highlightedChanged"
"""Item's highlight state changed flag."""
- HIGHLIGHTED_COLOR = 'highlightedColorChanged'
+ HIGHLIGHTED_COLOR = "highlightedColorChanged"
"""Deprecated, use HIGHLIGHTED_STYLE instead."""
- HIGHLIGHTED_STYLE = 'highlightedStyleChanged'
+ HIGHLIGHTED_STYLE = "highlightedStyleChanged"
"""Item's highlighted style changed flag."""
- SCALE = 'scaleChanged'
+ SCALE = "scaleChanged"
"""Item's scale changed flag."""
- TEXT = 'textChanged'
+ TEXT = "textChanged"
"""Item's text changed flag."""
- POSITION = 'positionChanged'
+ POSITION = "positionChanged"
"""Item's position changed flag.
This is emitted when a marker position changed and
when an image origin changed.
"""
- OVERLAY = 'overlayChanged'
+ OVERLAY = "overlayChanged"
"""Item's overlay state changed flag."""
- VISUALIZATION_MODE = 'visualizationModeChanged'
+ VISUALIZATION_MODE = "visualizationModeChanged"
"""Item's visualization mode changed flag."""
- COMPLEX_MODE = 'complexModeChanged'
+ COMPLEX_MODE = "complexModeChanged"
"""Item's complex data visualization mode changed flag."""
- NAME = 'nameChanged'
+ NAME = "nameChanged"
"""Item's name changed flag."""
- EDITABLE = 'editableChanged'
+ EDITABLE = "editableChanged"
"""Item's editable state changed flags."""
- SELECTABLE = 'selectableChanged'
+ SELECTABLE = "selectableChanged"
"""Item's selectable state changed flags."""
+ FONT = "fontChanged"
+ """Item's text font changed flag."""
+
+ BACKGROUND_COLOR = "backgroundColorChanged"
+ """Item's text background color changed flag."""
+
class Item(qt.QObject):
"""Description of an item of the plot"""
@@ -184,7 +192,7 @@ class Item(qt.QObject):
self._info = None
self._xlabel = None
self._ylabel = None
- self.__name = ''
+ self.__name = ""
self.__visibleBoundsTracking = False
self.__previousVisibleBounds = None
@@ -206,7 +214,7 @@ class Item(qt.QObject):
: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.')
+ 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()
@@ -240,8 +248,7 @@ class Item(qt.QObject):
if visible != self._visible:
self._visible = visible
# When visibility has changed, always mark as dirty
- self._updated(ItemChangedType.VISIBLE,
- checkVisibility=False)
+ self._updated(ItemChangedType.VISIBLE, checkVisibility=False)
if visible:
self._visibleBoundsChanged()
@@ -268,8 +275,7 @@ class Item(qt.QObject):
name = str(name)
if self.__name != name:
if self.getPlot() is not None:
- raise RuntimeError(
- "Cannot change name while item is in a PlotWidget")
+ raise RuntimeError("Cannot change name while item is in a PlotWidget")
self.__name = name
self._updated(ItemChangedType.NAME)
@@ -277,11 +283,6 @@ class Item(qt.QObject):
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
@@ -332,7 +333,8 @@ class Item(qt.QObject):
xmin, xmax = numpy.clip(bounds[:2], *plot.getXAxis().getLimits())
ymin, ymax = numpy.clip(
- bounds[2:], *plot.getYAxis(self.__getYAxis()).getLimits())
+ bounds[2:], *plot.getYAxis(self.__getYAxis()).getLimits()
+ )
if xmin == xmax or ymin == ymax: # Outside the plot area
return None
@@ -360,7 +362,7 @@ class Item(qt.QObject):
def __getYAxis(self) -> str:
"""Returns current Y axis ('left' or 'right')"""
- return self.getYAxis() if isinstance(self, YAxisMixIn) else 'left'
+ return self.getYAxis() if isinstance(self, YAxisMixIn) else "left"
def __connectToPlotWidget(self) -> None:
"""Connect to PlotWidget signals and install event filter"""
@@ -486,7 +488,7 @@ class Item(qt.QObject):
class DataItem(Item):
"""Item with a data extent in the plot"""
- def _boundsChanged(self, checkVisibility: bool=True) -> None:
+ def _boundsChanged(self, checkVisibility: bool = True) -> None:
"""Call this method in subclass when data bounds has changed.
:param bool checkVisibility:
@@ -506,6 +508,7 @@ class DataItem(Item):
self._boundsChanged(checkVisibility=False)
super().setVisible(visible)
+
# Mix-in classes ##############################################################
@@ -522,8 +525,7 @@ class ItemMixInBase(object):
: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")
+ raise RuntimeError("Issue with Mix-In class inheritance order")
class LabelsMixIn(ItemMixInBase):
@@ -597,7 +599,7 @@ class DraggableMixIn(ItemMixInBase):
raise NotImplementedError("Must be implemented in subclass")
-class ColormapMixIn(ItemMixInBase):
+class ColormapMixIn(_Colormappable, ItemMixInBase):
"""Mix-in class for items with colormap"""
def __init__(self):
@@ -631,8 +633,9 @@ class ColormapMixIn(ItemMixInBase):
"""Handle updates of the colormap"""
self._updated(ItemChangedType.COLORMAP)
- def _setColormappedData(self, data, copy=True,
- min_=None, minPositive=None, max_=None):
+ 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.
@@ -653,7 +656,10 @@ class ColormapMixIn(ItemMixInBase):
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_
+ self.__cacheColormapRange[Colormap.LOGARITHM, Colormap.MINMAX] = (
+ minPositive,
+ max_,
+ )
colormap = self.getColormap()
if None in (colormap.getVMin(), colormap.getVMax()):
@@ -705,26 +711,29 @@ class SymbolMixIn(ItemMixInBase):
_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')))
+ _SUPPORTED_SYMBOLS = dict(
+ (
+ ("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"),
+ ("\u2665", "Heart"),
+ ("", "None"),
+ )
+ )
"""Dict of supported symbols"""
def __init__(self):
@@ -799,7 +808,7 @@ class SymbolMixIn(ItemMixInBase):
symbol = symbolCode
break
else:
- raise ValueError('Unsupported symbol %s' % str(symbol))
+ raise ValueError("Unsupported symbol %s" % str(symbol))
if symbol != self._symbol:
self._symbol = symbol
@@ -826,50 +835,74 @@ class SymbolMixIn(ItemMixInBase):
self._updated(ItemChangedType.SYMBOL_SIZE)
+LineStyleType = Union[
+ str,
+ Tuple[Union[float, int], None],
+ Tuple[Union[float, int], Tuple[Union[float, int], Union[float, int]]],
+ Tuple[Union[float, int], Tuple[Union[float, int], Union[float, int], Union[float, int], Union[float, int]]],
+]
+"""Type for :class:`LineMixIn`'s line style"""
+
+
class LineMixIn(ItemMixInBase):
"""Mix-in class for item with line"""
- _DEFAULT_LINEWIDTH = 1.
+ _DEFAULT_LINEWIDTH: float = 1.0
"""Default line width"""
- _DEFAULT_LINESTYLE = '-'
+ _DEFAULT_LINESTYLE: LineStyleType = "-"
"""Default line style"""
- _SUPPORTED_LINESTYLE = '', ' ', '-', '--', '-.', ':', None
+ _SUPPORTED_LINESTYLE = "", " ", "-", "--", "-.", ":", None
"""Supported line styles"""
def __init__(self):
- self._linewidth = self._DEFAULT_LINEWIDTH
- self._linestyle = self._DEFAULT_LINESTYLE
+ self._linewidth: float = self._DEFAULT_LINEWIDTH
+ self._linestyle: LineStyleType = self._DEFAULT_LINESTYLE
@classmethod
- def getSupportedLineStyles(cls):
- """Returns list of supported line styles.
-
- :rtype: List[str,None]
- """
+ def getSupportedLineStyles(cls) -> tuple[str | None]:
+ """Returns list of supported constant line styles."""
return cls._SUPPORTED_LINESTYLE
- def getLineWidth(self):
- """Return the curve line width in pixels
-
- :rtype: float
- """
+ def getLineWidth(self) -> float:
+ """Return the curve line width in pixels"""
return self._linewidth
- def setLineWidth(self, width):
+ def setLineWidth(self, width: float):
"""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):
+ @classmethod
+ def isValidLineStyle(cls, style: LineStyleType | None) -> bool:
+ """Returns True for valid styles"""
+ if style is None or style in cls.getSupportedLineStyles():
+ return True
+ if not isinstance(style, tuple):
+ return False
+ if (
+ len(style) == 2
+ and isinstance(style[0], (float, int))
+ and (
+ style[1] is None
+ or style[1] == ()
+ or (
+ isinstance(style[1], tuple)
+ and len(style[1]) in (2, 4)
+ and all(map(lambda item: isinstance(item, (float, int)), style[1]))
+ )
+ )
+ ):
+ return True
+ return False
+
+ def getLineStyle(self) -> LineStyleType:
"""Return the type of the line
Type of line::
@@ -879,20 +912,19 @@ class LineMixIn(ItemMixInBase):
- '--' dashed line
- '-.' dash-dot line
- ':' dotted line
-
- :rtype: str
+ - (offset, (dash pattern))
"""
return self._linestyle
- def setLineStyle(self, style):
+ def setLineStyle(self, style: LineStyleType | None):
"""Set the style of the curve line.
See :meth:`getLineStyle`.
- :param str style: Line style
+ :param style: Line style
"""
- style = str(style)
- assert style in self.getSupportedLineStyles()
+ if not self.isValidLineStyle(style):
+ raise ValueError(f"No a valid line style: {style}")
if style is None:
style = self._DEFAULT_LINESTYLE
if style != self._linestyle:
@@ -903,7 +935,7 @@ class LineMixIn(ItemMixInBase):
class ColorMixIn(ItemMixInBase):
"""Mix-in class for item with color"""
- _DEFAULT_COLOR = (0., 0., 0., 1.)
+ _DEFAULT_COLOR = (0.0, 0.0, 0.0, 1.0)
"""Default color of the item"""
def __init__(self):
@@ -941,10 +973,43 @@ class ColorMixIn(ItemMixInBase):
self._updated(ItemChangedType.COLOR)
+class LineGapColorMixIn(ItemMixInBase):
+ """Mix-in class for dashed line gap color"""
+
+ _DEFAULT_LINE_GAP_COLOR = None
+ """Default dashed line gap color of the item"""
+
+ def __init__(self):
+ self.__lineGapColor = self._DEFAULT_LINE_GAP_COLOR
+
+ def getLineGapColor(self):
+ """Returns the RGBA color of dashed line gap of the item
+
+ :rtype: 4-tuple of float in [0, 1] or None
+ """
+ return self.__lineGapColor
+
+ def setLineGapColor(self, color):
+ """Set dashed line gap color
+
+ It supports:
+ - color names: e.g., 'green'
+ - color codes: '#RRGGBB' and '#RRGGBBAA'
+ - indexed color names: e.g., 'C0'
+ - RGB(A) sequence of uint8 in [0, 255] or float in [0, 1]
+ - QColor
+
+ :param color: line background color to be used
+ :type color: Union[str, List[int], List[float], QColor, None]
+ """
+ self.__lineGapColor = None if color is None else colors.rgba(color)
+ self._updated(ItemChangedType.LINE_GAP_COLOR)
+
+
class YAxisMixIn(ItemMixInBase):
"""Mix-in class for item with yaxis"""
- _DEFAULT_YAXIS = 'left'
+ _DEFAULT_YAXIS = "left"
"""Default Y axis the item belongs to"""
def __init__(self):
@@ -965,7 +1030,7 @@ class YAxisMixIn(ItemMixInBase):
:param str yaxis: 'left' or 'right'
"""
yaxis = str(yaxis)
- assert yaxis in ('left', 'right')
+ assert yaxis in ("left", "right")
if yaxis != self._yaxis:
self._yaxis = yaxis
# Handle data extent changed for DataItem
@@ -977,11 +1042,13 @@ class YAxisMixIn(ItemMixInBase):
# Switch Y axis signal connection
plot = self.getPlot()
if plot is not None:
- previousYAxis = 'left' if self.getXAxis() == 'right' else 'right'
+ previousYAxis = "left" if self.getXAxis() == "right" else "right"
plot.getYAxis(previousYAxis).sigLimitsChanged.disconnect(
- self._visibleBoundsChanged)
+ self._visibleBoundsChanged
+ )
plot.getYAxis(self.getYAxis()).sigLimitsChanged.connect(
- self._visibleBoundsChanged)
+ self._visibleBoundsChanged
+ )
self._visibleBoundsChanged()
self._updated(ItemChangedType.YAXIS)
@@ -1015,7 +1082,7 @@ class AlphaMixIn(ItemMixInBase):
"""Mix-in class for item with opacity"""
def __init__(self):
- self._alpha = 1.
+ self._alpha = 1.0
def getAlpha(self):
"""Returns the opacity of the item
@@ -1038,7 +1105,7 @@ class AlphaMixIn(ItemMixInBase):
:type alpha: float
"""
alpha = float(alpha)
- alpha = max(0., min(alpha, 1.)) # Clip alpha to [0., 1.] range
+ alpha = max(0.0, min(alpha, 1.0)) # Clip alpha to [0., 1.] range
if alpha != self._alpha:
self._alpha = alpha
self._updated(ItemChangedType.ALPHA)
@@ -1052,14 +1119,15 @@ class ComplexMixIn(ItemMixInBase):
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'
+
+ 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
@@ -1115,7 +1183,7 @@ class ComplexMixIn(ItemMixInBase):
elif mode is self.ComplexMode.SQUARE_AMPLITUDE:
return numpy.absolute(data) ** 2
else:
- raise ValueError('Unsupported conversion mode: %s', str(mode))
+ raise ValueError("Unsupported conversion mode: %s", str(mode))
@classmethod
def supportedComplexModes(cls):
@@ -1141,22 +1209,22 @@ class ScatterVisualizationMixIn(ItemMixInBase):
class Visualization(_Enum):
"""Different modes of scatter plot visualizations"""
- POINTS = 'points'
+ POINTS = "points"
"""Display scatter plot as a point cloud"""
- LINES = 'lines'
+ LINES = "lines"
"""Display scatter plot as a wireframe.
This is based on Delaunay triangulation
"""
- SOLID = 'solid'
+ SOLID = "solid"
"""Display scatter plot as a set of filled triangles.
This is based on Delaunay triangulation
"""
- REGULAR_GRID = 'regular_grid'
+ REGULAR_GRID = "regular_grid"
"""Display scatter plot as an image.
It expects the points to be the intersection of a regular grid,
@@ -1165,7 +1233,7 @@ class ScatterVisualizationMixIn(ItemMixInBase):
(either all lines from left to right or all from right to left).
"""
- IRREGULAR_GRID = 'irregular_grid'
+ IRREGULAR_GRID = "irregular_grid"
"""Display scatter plot as contiguous quadrilaterals.
It expects the points to be the intersection of an irregular grid,
@@ -1174,7 +1242,7 @@ class ScatterVisualizationMixIn(ItemMixInBase):
(either all lines from left to right or all from right to left).
"""
- BINNED_STATISTIC = 'binned_statistic'
+ BINNED_STATISTIC = "binned_statistic"
"""Display scatter plot as 2D binned statistic (i.e., generalized histogram).
"""
@@ -1182,13 +1250,13 @@ class ScatterVisualizationMixIn(ItemMixInBase):
class VisualizationParameter(_Enum):
"""Different parameter names for scatter plot visualizations"""
- GRID_MAJOR_ORDER = 'grid_major_order'
+ 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'
+ 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)).
@@ -1197,24 +1265,24 @@ class ScatterVisualizationMixIn(ItemMixInBase):
As for `GRID_SHAPE`, this can be wider than the current data.
"""
- GRID_SHAPE = 'grid_shape'
+ 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'
+ BINNED_STATISTIC_SHAPE = "binned_statistic_shape"
"""The number of bins in each dimension (height, width).
"""
- BINNED_STATISTIC_FUNCTION = 'binned_statistic_function'
+ 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'
+ 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)).
@@ -1225,8 +1293,8 @@ class ScatterVisualizationMixIn(ItemMixInBase):
"""
_SUPPORTED_VISUALIZATION_PARAMETER_VALUES = {
- VisualizationParameter.GRID_MAJOR_ORDER: ('row', 'column'),
- VisualizationParameter.BINNED_STATISTIC_FUNCTION: ('mean', 'count', 'sum'),
+ VisualizationParameter.GRID_MAJOR_ORDER: ("row", "column"),
+ VisualizationParameter.BINNED_STATISTIC_FUNCTION: ("mean", "count", "sum"),
}
"""Supported visualization parameter values.
@@ -1235,9 +1303,12 @@ class ScatterVisualizationMixIn(ItemMixInBase):
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'
+ 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):
@@ -1263,8 +1334,7 @@ class ScatterVisualizationMixIn(ItemMixInBase):
:returns: tuple of supported of values or None if not defined.
"""
parameter = cls.VisualizationParameter(parameter)
- return cls._SUPPORTED_VISUALIZATION_PARAMETER_VALUES.get(
- parameter, None)
+ return cls._SUPPORTED_VISUALIZATION_PARAMETER_VALUES.get(parameter, None)
def setVisualization(self, mode):
"""Set the scatter plot visualization mode to use.
@@ -1351,6 +1421,7 @@ class ScatterVisualizationMixIn(ItemMixInBase):
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
@@ -1398,22 +1469,18 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
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)
+ 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
+ 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)
+ error = numpy.array(error, copy=True, dtype=numpy.float64)
else:
_logger.error("Unhandled error array")
@@ -1437,16 +1504,17 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
if xPositive:
x = self.getXData(copy=False)
- with numpy.errstate(invalid='ignore'): # Ignore NaN warnings
+ 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
+ with numpy.errstate(invalid="ignore"): # Ignore NaN warnings
yclipped = y <= 0
- self._clippedCache[(xPositive, yPositive)] = \
- numpy.logical_or(xclipped, yclipped)
+ self._clippedCache[(xPositive, yPositive)] = numpy.logical_or(
+ xclipped, yclipped
+ )
return self._clippedCache[(xPositive, yPositive)]
def _logFilterData(self, xPositive, yPositive):
@@ -1484,7 +1552,7 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
def __minMaxDataWithError(
data: numpy.ndarray,
error: Optional[Union[float, numpy.ndarray]],
- positiveOnly: bool
+ positiveOnly: bool,
) -> Tuple[float]:
if error is None:
min_, max_ = min_max(data, finite=True)
@@ -1532,9 +1600,12 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
xmin, xmax = self.__minMaxDataWithError(x, xerror, xPositive)
ymin, ymax = self.__minMaxDataWithError(y, yerror, yPositive)
- self._boundsCache[(xPositive, yPositive)] = tuple([
- (bound if bound is not None else numpy.nan)
- for bound in (xmin, xmax, ymin, ymax)])
+ 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):
@@ -1548,8 +1619,9 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
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)
+ self._filteredCache[(xPositive, yPositive)] = self._logFilterData(
+ xPositive, yPositive
+ )
return self._filteredCache[(xPositive, yPositive)]
return None
@@ -1570,10 +1642,12 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
if cached_data is not None:
return cached_data
- return (self.getXData(copy),
- self.getYData(copy),
- self.getXErrorData(copy),
- self.getYErrorData(copy))
+ 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
@@ -1640,12 +1714,10 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
# Convert complex data
if numpy.iscomplexobj(x):
- _logger.warning(
- 'Converting x data to absolute value to plot it.')
+ _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.')
+ _logger.warning("Converting y data to absolute value to plot it.")
y = numpy.absolute(y)
if xerror is not None:
@@ -1653,7 +1725,8 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
xerror = numpy.array(xerror, copy=copy)
if numpy.iscomplexobj(xerror):
_logger.warning(
- 'Converting xerror data to absolute value to plot it.')
+ "Converting xerror data to absolute value to plot it."
+ )
xerror = numpy.absolute(xerror)
else:
xerror = float(xerror)
@@ -1662,7 +1735,8 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
yerror = numpy.array(yerror, copy=copy)
if numpy.iscomplexobj(yerror):
_logger.warning(
- 'Converting yerror data to absolute value to plot it.')
+ "Converting yerror data to absolute value to plot it."
+ )
yerror = numpy.absolute(yerror)
else:
yerror = float(yerror)
@@ -1691,7 +1765,7 @@ class BaselineMixIn(object):
:param baseline: baseline value(s)
:type: Union[None,float,numpy.ndarray]
"""
- if (isinstance(baseline, abc.Iterable)):
+ if isinstance(baseline, abc.Iterable):
baseline = numpy.array(baseline)
self._baseline = baseline
@@ -1713,7 +1787,6 @@ class _Style:
class HighlightedMixIn(ItemMixInBase):
-
def __init__(self):
self._highlightStyle = self._DEFAULT_HIGHLIGHT_STYLE
self._highlighted = False
diff --git a/src/silx/gui/plot/items/curve.py b/src/silx/gui/plot/items/curve.py
index 93e4719..e8d0d52 100644
--- a/src/silx/gui/plot/items/curve.py
+++ b/src/silx/gui/plot/items/curve.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -23,6 +23,7 @@
# ###########################################################################*/
"""This module provides the :class:`Curve` item of the :class:`Plot`.
"""
+from __future__ import annotations
__authors__ = ["T. Vincent"]
__license__ = "MIT"
@@ -33,11 +34,22 @@ import logging
import numpy
-from ....utils.deprecation import deprecated
+from ....utils.deprecation import deprecated_warning
from ... import colors
-from .core import (PointsBase, LabelsMixIn, ColorMixIn, YAxisMixIn,
- FillMixIn, LineMixIn, SymbolMixIn, ItemChangedType,
- BaselineMixIn, HighlightedMixIn, _Style)
+from .core import (
+ PointsBase,
+ LabelsMixIn,
+ ColorMixIn,
+ YAxisMixIn,
+ FillMixIn,
+ LineMixIn,
+ LineGapColorMixIn,
+ LineStyleType,
+ SymbolMixIn,
+ BaselineMixIn,
+ HighlightedMixIn,
+ _Style,
+)
_logger = logging.getLogger(__name__)
@@ -49,14 +61,22 @@ class CurveStyle(_Style):
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
+ :param linestyle: Style of the line
+ :param linewidth: Width of the line
+ :param symbol: Symbol for markers
+ :param symbolsize: Size of the markers
+ :param gapcolor: Color of gaps of dashed line
"""
- def __init__(self, color=None, linestyle=None, linewidth=None,
- symbol=None, symbolsize=None):
+ def __init__(
+ self,
+ color: colors.RGBAColorType | None = None,
+ linestyle: LineStyleType | None = None,
+ linewidth: float | None = None,
+ symbol: str | None = None,
+ symbolsize: float | None = None,
+ gapcolor: colors.RGBAColorType | None = None,
+ ):
if color is None:
self._color = None
else:
@@ -68,8 +88,8 @@ class CurveStyle(_Style):
color = colors.rgba(color)
self._color = color
- if linestyle is not None:
- assert linestyle in LineMixIn.getSupportedLineStyles()
+ if not LineMixIn.isValidLineStyle(linestyle):
+ raise ValueError(f"Not a valid line style: {linestyle}")
self._linestyle = linestyle
self._linewidth = None if linewidth is None else float(linewidth)
@@ -80,6 +100,8 @@ class CurveStyle(_Style):
self._symbolsize = None if symbolsize is None else float(symbolsize)
+ self._gapcolor = None if gapcolor is None else colors.rgba(gapcolor)
+
def getColor(self, copy=True):
"""Returns the color or None if not set.
@@ -93,7 +115,14 @@ class CurveStyle(_Style):
else:
return self._color
- def getLineStyle(self):
+ def getLineGapColor(self):
+ """Returns the color of dashed line gaps or None if not set.
+
+ :rtype: Union[List[float],None]
+ """
+ return self._gapcolor
+
+ def getLineStyle(self) -> LineStyleType | None:
"""Return the type of the line or None if not set.
Type of line::
@@ -103,8 +132,7 @@ class CurveStyle(_Style):
- '--' dashed line
- '-.' dash-dot line
- ':' dotted line
-
- :rtype: Union[str,None]
+ - (offset, (dash pattern))
"""
return self._linestyle
@@ -141,17 +169,29 @@ class CurveStyle(_Style):
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())
+ 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()
+ and self.getLineGapColor() == other.getLineGapColor()
+ )
else:
return False
-class Curve(PointsBase, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn,
- LineMixIn, BaselineMixIn, HighlightedMixIn):
+class Curve(
+ PointsBase,
+ ColorMixIn,
+ YAxisMixIn,
+ FillMixIn,
+ LabelsMixIn,
+ LineMixIn,
+ LineGapColorMixIn,
+ BaselineMixIn,
+ HighlightedMixIn,
+):
"""Description of a curve"""
_DEFAULT_Z_LAYER = 1
@@ -160,13 +200,13 @@ class Curve(PointsBase, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn,
_DEFAULT_SELECTABLE = True
"""Default selectable state for curves"""
- _DEFAULT_LINEWIDTH = 1.
+ _DEFAULT_LINEWIDTH = 1.0
"""Default line width of the curve"""
- _DEFAULT_LINESTYLE = '-'
+ _DEFAULT_LINESTYLE = "-"
"""Default line style of the curve"""
- _DEFAULT_HIGHLIGHT_STYLE = CurveStyle(color='black')
+ _DEFAULT_HIGHLIGHT_STYLE = CurveStyle(color="black")
"""Default highlight style of the item"""
_DEFAULT_BASELINE = None
@@ -178,6 +218,7 @@ class Curve(PointsBase, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn,
FillMixIn.__init__(self)
LabelsMixIn.__init__(self)
LineMixIn.__init__(self)
+ LineGapColorMixIn.__init__(self)
BaselineMixIn.__init__(self)
HighlightedMixIn.__init__(self)
@@ -186,29 +227,38 @@ class Curve(PointsBase, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn,
def _addBackendRenderer(self, backend):
"""Update backend renderer"""
# Filter-out values <= 0
- xFiltered, yFiltered, xerror, yerror = self.getData(
- copy=False, displayed=True)
+ 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))
+ return backend.addCurve(
+ xFiltered,
+ yFiltered,
+ color=style.getColor(),
+ gapcolor=style.getLineGapColor(),
+ 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"""
+ deprecated_warning(
+ "Attributes",
+ "__getitem__",
+ since_version="2.0.0",
+ replacement="Use Curve methods",
+ )
if isinstance(item, slice):
return [self[index] for index in range(*item.indices(5))]
elif item == 0:
@@ -222,44 +272,24 @@ class Curve(PointsBase, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn,
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(),
+ "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.
@@ -274,32 +304,26 @@ class Curve(PointsBase, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn,
linewidth = style.getLineWidth()
symbol = style.getSymbol()
symbolsize = style.getSymbolSize()
+ gapcolor = style.getLineGapColor()
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)
+ symbolsize=self.getSymbolSize() if symbolsize is None else symbolsize,
+ gapcolor=self.getLineGapColor() if gapcolor is None else gapcolor,
+ )
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()
+ return CurveStyle(
+ color=self.getColor(),
+ linestyle=self.getLineStyle(),
+ linewidth=self.getLineWidth(),
+ symbol=self.getSymbol(),
+ symbolsize=self.getSymbolSize(),
+ gapcolor=self.getLineGapColor(),
+ )
def setData(self, x, y, xerror=None, yerror=None, baseline=None, copy=True):
"""Set the data of the curve.
@@ -319,6 +343,5 @@ class Curve(PointsBase, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn,
: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)
+ PointsBase.setData(self, x=x, y=y, xerror=xerror, yerror=yerror, copy=copy)
self._setBaseline(baseline=baseline)
diff --git a/src/silx/gui/plot/items/histogram.py b/src/silx/gui/plot/items/histogram.py
index 007f0c7..1dc851b 100644
--- a/src/silx/gui/plot/items/histogram.py
+++ b/src/silx/gui/plot/items/histogram.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -32,15 +32,20 @@ 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 collections import abc
from ....utils.proxy import docstring
-from .core import (DataItem, AlphaMixIn, BaselineMixIn, ColorMixIn, FillMixIn,
- LineMixIn, YAxisMixIn, ItemChangedType, Item)
+from .core import (
+ DataItem,
+ AlphaMixIn,
+ BaselineMixIn,
+ ColorMixIn,
+ FillMixIn,
+ LineMixIn,
+ LineGapColorMixIn,
+ YAxisMixIn,
+ ItemChangedType,
+)
from ._pick import PickingResult
_logger = logging.getLogger(__name__)
@@ -62,17 +67,17 @@ def _computeEdges(x, histogramType):
"""
# for now we consider that the spaces between xs are constant
edges = x.copy()
- if histogramType == 'left':
+ 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')
+ 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':
+ if histogramType == "right":
width = 1
if len(x) > 1:
width = x[-1] - x[-2]
@@ -102,8 +107,16 @@ def _getHistogramCurve(histogram, edges):
# TODO: Yerror, test log scale
-class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
- LineMixIn, YAxisMixIn, BaselineMixIn):
+class Histogram(
+ DataItem,
+ AlphaMixIn,
+ ColorMixIn,
+ FillMixIn,
+ LineMixIn,
+ LineGapColorMixIn,
+ YAxisMixIn,
+ BaselineMixIn,
+):
"""Description of an histogram"""
_DEFAULT_Z_LAYER = 1
@@ -112,10 +125,10 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
_DEFAULT_SELECTABLE = False
"""Default selectable state for histograms"""
- _DEFAULT_LINEWIDTH = 1.
+ _DEFAULT_LINEWIDTH = 1.0
"""Default line width of the histogram"""
- _DEFAULT_LINESTYLE = '-'
+ _DEFAULT_LINESTYLE = "-"
"""Default line style of the histogram"""
_DEFAULT_BASELINE = None
@@ -127,6 +140,7 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
ColorMixIn.__init__(self)
FillMixIn.__init__(self)
LineMixIn.__init__(self)
+ LineGapColorMixIn.__init__(self)
YAxisMixIn.__init__(self)
self._histogram = ()
@@ -156,26 +170,30 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
if xPositive or yPositive:
clipped = numpy.logical_or(
- (x <= 0) if xPositive else False,
- (y <= 0) if yPositive else False)
+ (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)
+ return backend.addCurve(
+ x,
+ y,
+ color=self.getColor(),
+ gapcolor=self.getLineGapColor(),
+ 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)
@@ -193,11 +211,10 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
if xPositive:
# Replace edges <= 0 by NaN and corresponding values by NaN
- clipped_edges = (edges <= 0)
+ 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:])
+ clipped_values = numpy.logical_or(clipped_edges[:-1], clipped_edges[1:])
else:
clipped_values = numpy.zeros_like(values, dtype=bool)
@@ -208,20 +225,26 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
values[clipped_values] = numpy.nan
if yPositive:
- return (numpy.nanmin(edges),
- numpy.nanmax(edges),
- numpy.nanmin(values),
- numpy.nanmax(values))
+ 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]:
+ 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
@@ -241,7 +264,7 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
# Check x
edges = self.getBinEdgesData(copy=False)
- index = numpy.searchsorted(edges, (xData,), side='left')[0] - 1
+ index = numpy.searchsorted(edges, (xData,), side="left")[0] - 1
# Safe indexing in histogram values
index = numpy.clip(index, 0, len(edges) - 2)
@@ -251,8 +274,9 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
baseline = 0 # Default value
value = self.getValueData(copy=False)[index]
- if ((baseline <= value and baseline <= yData <= value) or
- (value < baseline and value <= yData <= baseline)):
+ if (baseline <= value and baseline <= yData <= value) or (
+ value < baseline and value <= yData <= baseline
+ ):
return PickingResult(self, numpy.array([index]))
else:
return None
@@ -296,12 +320,13 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
:returns: (N histogram value, N+1 bin edges)
:rtype: 2-tuple of numpy.nadarray
"""
- return (self.getValueData(copy),
- self.getBinEdgesData(copy),
- self.getBaseline(copy))
+ return (
+ self.getValueData(copy),
+ self.getBinEdgesData(copy),
+ self.getBaseline(copy),
+ )
- def setData(self, histogram, edges, align='center', baseline=None,
- copy=True):
+ 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.
@@ -324,7 +349,7 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
assert histogram.ndim == 1
assert edges.ndim == 1
assert edges.size in (histogram.size, histogram.size + 1)
- assert align in ('center', 'left', 'right')
+ assert align in ("center", "left", "right")
if histogram.size == 0: # No data
self._histogram = ()
@@ -338,12 +363,12 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
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)):
+ 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
+ new_baseline[i_value * 2 : i_value * 2 + 2] = value
baseline = new_baseline
self._histogram = histogram
self._edges = edges
@@ -376,11 +401,11 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
"""
# for now we consider that the spaces between xs are constant
edges = x.copy()
- if histogramType == 'left':
+ if histogramType == "left":
return edges[1:]
- if histogramType == 'center':
+ if histogramType == "center":
edges = (edges[1:] + edges[:-1]) / 2.0
- if histogramType == 'right':
+ if histogramType == "right":
width = 1
if len(x) > 1:
width = x[-1] + x[-2]
diff --git a/src/silx/gui/plot/items/image.py b/src/silx/gui/plot/items/image.py
index eaee05a..18310d9 100644
--- a/src/silx/gui/plot/items/image.py
+++ b/src/silx/gui/plot/items/image.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -29,17 +29,21 @@ __authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "08/12/2020"
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
+from collections import abc
import logging
import numpy
from ....utils.proxy import docstring
-from .core import (DataItem, LabelsMixIn, DraggableMixIn, ColormapMixIn,
- AlphaMixIn, ItemChangedType)
+from ....utils.deprecation import deprecated_warning
+from .core import (
+ DataItem,
+ LabelsMixIn,
+ DraggableMixIn,
+ ColormapMixIn,
+ AlphaMixIn,
+ ItemChangedType,
+)
_logger = logging.getLogger(__name__)
@@ -62,23 +66,22 @@ def _convertImageToRgba32(image, copy=True):
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
+ if image.dtype.name != "uint8":
+ if image.dtype.kind == "f": # Float in [0, 1]
+ image = (numpy.clip(image, 0.0, 1.0) * 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
+ 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)
+ 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
+ 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)
@@ -100,11 +103,17 @@ class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
self._data = data
self._mask = mask
self.__valueDataCache = None # Store default data
- self._origin = (0., 0.)
- self._scale = (1., 1.)
+ self._origin = (0.0, 0.0)
+ self._scale = (1.0, 1.0)
def __getitem__(self, item):
"""Compatibility with PyMca and silx <= 0.4.0"""
+ deprecated_warning(
+ "Attributes",
+ "__getitem__",
+ since_version="2.0.0",
+ replacement="Use ImageBase methods",
+ )
if isinstance(item, slice):
return [self[index] for index in range(*item.indices(5))]
elif item == 0:
@@ -118,15 +127,15 @@ class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
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(),
+ "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:
@@ -167,8 +176,7 @@ class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
@docstring(DraggableMixIn)
def drag(self, from_, to):
origin = self.getOrigin()
- self.setOrigin((origin[0] + to[0] - from_[0],
- origin[1] + to[1] - from_[1]))
+ self.setOrigin((origin[0] + to[0] - from_[0], origin[1] + to[1] - from_[1]))
def getData(self, copy=True):
"""Returns the image data
@@ -190,8 +198,10 @@ class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
self._boundsChanged()
self._updated(ItemChangedType.DATA)
- if (self.getMaskData(copy=False) is not None and
- previousShape != self._data.shape):
+ 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)
@@ -211,7 +221,9 @@ class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
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]]
+ 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)
@@ -228,7 +240,9 @@ class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
shape = self.getData(copy=False).shape[:2]
if mask.shape != shape:
- _logger.warning("Inconsistent shape between mask and data %s, %s", 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
@@ -278,7 +292,7 @@ class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
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')
+ raise NotImplementedError("This MUST be implemented in sub-class")
def getOrigin(self):
"""Returns the offset from origin at which to display the image.
@@ -336,9 +350,11 @@ class ImageDataBase(ImageBase, ColormapMixIn):
def _getColormapForRendering(self):
colormap = self.getColormap()
if colormap.isAutoscale():
+ # NOTE: Make sure getColormapRange comes from the original object
+ vrange = colormap.getColormapRange(self)
# Avoid backend to compute autoscale: use item cache
colormap = colormap.copy()
- colormap.setVRange(*colormap.getColormapRange(self))
+ colormap.setVRange(*vrange)
return colormap
def getRgbaImageData(self, copy=True):
@@ -350,7 +366,7 @@ class ImageDataBase(ImageBase, ColormapMixIn):
return self.getColormap().applyToData(self)
def setData(self, data, copy=True):
- """"Set the image data
+ """Set the image data
:param numpy.ndarray data: Data array with 2 dimensions (h, w)
:param bool copy: True (Default) to get a copy,
@@ -358,13 +374,11 @@ class ImageDataBase(ImageBase, ColormapMixIn):
"""
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.')
+ 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.')
+ _logger.warning("Converting complex image to absolute value to plot it.")
data = numpy.absolute(data)
super().setData(data)
@@ -391,8 +405,10 @@ class ImageData(ImageDataBase):
# 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):
+ 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)
@@ -400,20 +416,28 @@ class ImageData(ImageDataBase):
if dataToUse.size == 0:
return None # No data to display
- return backend.addImage(dataToUse,
- origin=self.getOrigin(),
- scale=self.getScale(),
- colormap=self._getColormapForRendering(),
- alpha=self.getAlpha())
+ return backend.addImage(
+ dataToUse,
+ origin=self.getOrigin(),
+ scale=self.getScale(),
+ colormap=self._getColormapForRendering(),
+ alpha=self.getAlpha(),
+ )
def __getitem__(self, item):
"""Compatibility with PyMca and silx <= 0.4.0"""
+ deprecated_warning(
+ "Attributes",
+ "__getitem__",
+ since_version="2.0.0",
+ replacement="Use ImageData methods",
+ )
if item == 3:
return self.getAlternativeImageData(copy=False)
params = ImageBase.__getitem__(self, item)
if item == 4:
- params['colormap'] = self.getColormap()
+ params["colormap"] = self.getColormap()
return params
@@ -431,7 +455,7 @@ class ImageData(ImageDataBase):
alphaImage = self.getAlphaData(copy=False)
if alphaImage is not None:
# Apply transparency
- image[:,:, 3] = image[:,:, 3] * alphaImage
+ image[:, :, 3] = image[:, :, 3] * alphaImage
return image
def getAlternativeImageData(self, copy=True):
@@ -459,7 +483,7 @@ class ImageData(ImageDataBase):
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
+ """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,
@@ -484,10 +508,10 @@ class ImageData(ImageDataBase):
if alpha is not None:
alpha = numpy.array(alpha, copy=copy)
assert alpha.shape == data.shape
- if alpha.dtype.kind != 'f':
+ 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.)
+ if numpy.any(numpy.logical_or(alpha < 0.0, alpha > 1.0)):
+ alpha = numpy.clip(alpha, 0.0, 1.0)
self.__alpha = alpha
super().setData(data)
@@ -512,11 +536,13 @@ class ImageRgba(ImageBase):
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())
+ 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
@@ -533,8 +559,14 @@ class ImageRgba(ImageBase):
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)
+ if data.ndim != 3:
+ raise ValueError(
+ f"RGB(A) image is expected to be a 3D dataset. Got {data.ndim} dimensions"
+ )
+ if data.shape[-1] not in (3, 4):
+ raise ValueError(
+ f"RGB(A) image is expected to have 3 or 4 elements as last dimension. Got {data.shape[-1]}"
+ )
super().setData(data)
def _getValueData(self, copy=True):
@@ -545,10 +577,10 @@ class ImageRgba(ImageBase):
: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.
+ intensity = (
+ rgba[:, :, 0] * 0.299 + rgba[:, :, 1] * 0.587 + rgba[:, :, 2] * 0.114
+ )
+ intensity *= rgba[:, :, 3] / 255.0
return intensity
@@ -558,6 +590,7 @@ class MaskImageData(ImageData):
This class is used to flag mask items. This information is used to improve
internal silx widgets.
"""
+
pass
diff --git a/src/silx/gui/plot/items/image_aggregated.py b/src/silx/gui/plot/items/image_aggregated.py
index ffd41b2..b35e00a 100644
--- a/src/silx/gui/plot/items/image_aggregated.py
+++ b/src/silx/gui/plot/items/image_aggregated.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2021 European Synchrotron Radiation Facility
+# Copyright (c) 2021-2023 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
@@ -31,6 +31,7 @@ __date__ = "07/07/2021"
import enum
import logging
from typing import Tuple, Union
+import warnings
import numpy
@@ -68,7 +69,7 @@ class ImageDataAggregated(ImageDataBase):
self.__currentLOD = 0, 0
self.__aggregationMode = self.Aggregation.NONE
- def setAggregationMode(self, mode: Union[str,Aggregation]):
+ def setAggregationMode(self, mode: Union[str, Aggregation]):
"""Set the aggregation method used to reduce the data to screen resolution.
:param Aggregation mode: The aggregation method
@@ -115,12 +116,14 @@ class ImageDataAggregated(ImageDataBase):
if (lodx, lody) not in self.__cacheLODData:
height, width = data.shape
- self.__cacheLODData[(lodx, lody)] = aggregator(
- data[: (height // lody) * lody, : (width // lodx) * lodx].reshape(
- height // lody, lody, width // lodx, lodx
- ),
- axis=(1, 3),
- )
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", category=RuntimeWarning)
+ self.__cacheLODData[(lodx, lody)] = aggregator(
+ data[
+ : (height // lody) * lody, : (width // lodx) * lodx
+ ].reshape(height // lody, lody, width // lodx, lodx),
+ axis=(1, 3),
+ )
self.__currentLOD = lodx, lody
displayedData = self.__cacheLODData[self.__currentLOD]
@@ -153,10 +156,7 @@ class ImageDataAggregated(ImageDataBase):
xaxis = plot.getXAxis()
yaxis = plot.getYAxis(axis)
- if (
- xaxis.getScale() != Axis.LINEAR
- or yaxis.getScale() != Axis.LINEAR
- ):
+ if xaxis.getScale() != Axis.LINEAR or yaxis.getScale() != Axis.LINEAR:
raise RuntimeError("Only available with linear axes")
xmin, xmax = xaxis.getLimits()
@@ -200,8 +200,10 @@ class ImageDataAggregated(ImageDataBase):
def __plotLimitsChanged(self):
"""Trigger update if level of details has changed"""
- if (self.getAggregationMode() != self.Aggregation.NONE and
- self.__currentLOD != self._getLevelOfDetails()):
+ if (
+ self.getAggregationMode() != self.Aggregation.NONE
+ and self.__currentLOD != self._getLevelOfDetails()
+ ):
self._updated()
@docstring(ImageDataBase)
diff --git a/src/silx/gui/plot/items/marker.py b/src/silx/gui/plot/items/marker.py
index 7596eb0..b3da451 100755
--- a/src/silx/gui/plot/items/marker.py
+++ b/src/silx/gui/plot/items/marker.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -23,6 +23,7 @@
# ###########################################################################*/
"""This module provides markers item of the :class:`Plot`.
"""
+from __future__ import annotations
__authors__ = ["T. Vincent"]
__license__ = "MIT"
@@ -30,11 +31,22 @@ __date__ = "06/03/2017"
import logging
+import numpy
from ....utils.proxy import docstring
-from .core import (Item, DraggableMixIn, ColorMixIn, LineMixIn, SymbolMixIn,
- ItemChangedType, YAxisMixIn)
+from .core import (
+ Item,
+ DraggableMixIn,
+ ColorMixIn,
+ LineMixIn,
+ SymbolMixIn,
+ ItemChangedType,
+ YAxisMixIn,
+)
+from silx import config
from silx.gui import qt
+from silx.gui import colors
+
_logger = logging.getLogger(__name__)
@@ -47,7 +59,7 @@ class MarkerBase(Item, DraggableMixIn, ColorMixIn, YAxisMixIn):
sigDragFinished = qt.Signal()
"""Signal emitted when the marker is released"""
- _DEFAULT_COLOR = (0., 0., 0., 1.)
+ _DEFAULT_COLOR = (0.0, 0.0, 0.0, 1.0)
"""Default color of the markers"""
def __init__(self):
@@ -56,14 +68,21 @@ class MarkerBase(Item, DraggableMixIn, ColorMixIn, YAxisMixIn):
ColorMixIn.__init__(self)
YAxisMixIn.__init__(self)
- self._text = ''
+ self._text = ""
+ self._font = None
+ if config.DEFAULT_PLOT_MARKER_TEXT_FONT_SIZE is not None:
+ self._font = qt.QFont(
+ qt.QApplication.instance().font().family(),
+ config.DEFAULT_PLOT_MARKER_TEXT_FONT_SIZE,
+ )
+
self._x = None
self._y = None
+ self._bgColor: colors.RGBAColorType | None = None
self._constraint = self._defaultConstraint
self.__isBeingDragged = False
- def _addRendererCall(self, backend,
- symbol=None, linestyle='-', linewidth=1):
+ def _addRendererCall(self, backend, symbol=None, linestyle="-", linewidth=1):
"""Perform the update of the backend renderer"""
return backend.addMarker(
x=self.getXPosition(),
@@ -74,7 +93,10 @@ class MarkerBase(Item, DraggableMixIn, ColorMixIn, YAxisMixIn):
linestyle=linestyle,
linewidth=linewidth,
constraint=self.getConstraint(),
- yaxis=self.getYAxis())
+ yaxis=self.getYAxis(),
+ font=self._font, # Do not use getFont to spare creating a new QFont
+ bgcolor=self.getBackgroundColor(),
+ )
def _addBackendRenderer(self, backend):
"""Update backend renderer"""
@@ -108,6 +130,39 @@ class MarkerBase(Item, DraggableMixIn, ColorMixIn, YAxisMixIn):
self._text = text
self._updated(ItemChangedType.TEXT)
+ def getFont(self) -> qt.QFont | None:
+ """Returns a copy of the QFont used to render text.
+
+ To modify the text font, use :meth:`setFont`.
+ """
+ return None if self._font is None else qt.QFont(self._font)
+
+ def setFont(self, font: qt.QFont | None):
+ """Set the QFont used to render text, use None for default.
+
+ A copy is stored, so further modification of the provided font are not taken into account.
+ """
+ if font != self._font:
+ self._font = None if font is None else qt.QFont(font)
+ self._updated(ItemChangedType.FONT)
+
+ def getBackgroundColor(self) -> colors.RGBAColorType | None:
+ """Returns the RGBA background color of the item"""
+ return self._bgColor
+
+ def setBackgroundColor(self, color):
+ """Set item text background color
+
+ :param color: color(s) to be used as a str ("#RRGGBB") or (npoints, 4)
+ unsigned byte array or one of the predefined color names
+ defined in colors.py
+ """
+ if color is not None:
+ color = colors.rgba(color)
+ if self._bgColor != color:
+ self._bgColor = color
+ self._updated(ItemChangedType.BACKGROUND_COLOR)
+
def getXPosition(self):
"""Returns the X position of the marker line in data coordinates
@@ -122,14 +177,14 @@ class MarkerBase(Item, DraggableMixIn, ColorMixIn, YAxisMixIn):
"""
return self._y
- def getPosition(self):
+ def getPosition(self) -> tuple[float | None, float | None]:
"""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):
+ def setPosition(self, x: float, y: float):
"""Set marker position in data coordinates
Constraint are applied if any.
@@ -188,15 +243,15 @@ class MarkerBase(Item, DraggableMixIn, ColorMixIn, YAxisMixIn):
class Marker(MarkerBase, SymbolMixIn):
"""Description of a marker"""
- _DEFAULT_SYMBOL = '+'
+ _DEFAULT_SYMBOL = "+"
"""Default symbol of the marker"""
def __init__(self):
MarkerBase.__init__(self)
SymbolMixIn.__init__(self)
- self._x = 0.
- self._y = 0.
+ self._x = 0.0
+ self._y = 0.0
def _addBackendRenderer(self, backend):
return self._addRendererCall(backend, symbol=self.getSymbol())
@@ -209,9 +264,9 @@ class Marker(MarkerBase, SymbolMixIn):
:param constraint: The constraint of the dragging of this marker
:type: constraint: callable or str
"""
- if constraint == 'horizontal':
+ if constraint == "horizontal":
constraint = self._horizontalConstraint
- elif constraint == 'vertical':
+ elif constraint == "vertical":
constraint = self._verticalConstraint
super(Marker, self)._setConstraint(constraint)
@@ -231,9 +286,9 @@ class _LineMarker(MarkerBase, LineMixIn):
LineMixIn.__init__(self)
def _addBackendRenderer(self, backend):
- return self._addRendererCall(backend,
- linestyle=self.getLineStyle(),
- linewidth=self.getLineWidth())
+ return self._addRendererCall(
+ backend, linestyle=self.getLineStyle(), linewidth=self.getLineWidth()
+ )
class XMarker(_LineMarker):
@@ -241,7 +296,7 @@ class XMarker(_LineMarker):
def __init__(self):
_LineMarker.__init__(self)
- self._x = 0.
+ self._x = 0.0
def setPosition(self, x, y):
"""Set marker line position in data coordinates
@@ -263,7 +318,7 @@ class YMarker(_LineMarker):
def __init__(self):
_LineMarker.__init__(self)
- self._y = 0.
+ self._y = 0.0
def setPosition(self, x, y):
"""Set marker line position in data coordinates
diff --git a/src/silx/gui/plot/items/roi.py b/src/silx/gui/plot/items/roi.py
index 559e7e0..7390b88 100644
--- a/src/silx/gui/plot/items/roi.py
+++ b/src/silx/gui/plot/items/roi.py
@@ -35,6 +35,7 @@ __date__ = "28/06/2018"
import logging
import numpy
+from typing import Tuple
from ... import utils
from .. import items
@@ -60,15 +61,15 @@ 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'
+ 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 = "+"
"""Default symbol of the PointROI
It overwrite the `SymbolMixIn` class attribte.
@@ -88,30 +89,26 @@ class PointROI(RegionOfInterest, items.SymbolMixIn):
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:
+ if event == items.ItemChangedType.EDITABLE:
self._marker._setDraggable(self.isEditable())
- elif event in [items.ItemChangedType.VISIBLE,
- items.ItemChangedType.SELECTABLE]:
+ elif event in [items.ItemChangedType.VISIBLE, items.ItemChangedType.SELECTABLE]:
self._updateItemProperty(event, self, self._marker)
super(PointROI, self)._updated(event, checkVisibility)
+ def _updateText(self, text: str):
+ self._marker.setText(text)
+
def _updatedStyle(self, event, style):
self._marker.setColor(style.getColor())
- def getPosition(self):
- """Returns the position of this ROI
-
- :rtype: numpy.ndarray
- """
+ def getPosition(self) -> Tuple[float, float]:
+ """Returns the position of this ROI"""
return self._marker.getPosition()
def setPosition(self, pos):
"""Set the position of this ROI
- :param numpy.ndarray pos: 2d-coordinate of this point
+ :param pos: 2d-coordinate of this point
"""
self._marker.setPosition(*pos)
@@ -126,16 +123,15 @@ class PointROI(RegionOfInterest, items.SymbolMixIn):
self.sigRegionChanged.emit()
def __str__(self):
- params = '%f %f' % self.getPosition()
+ 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
- """
+ """A ROI identifying a point in a 2D plot and displayed as a cross"""
- ICON = 'add-shape-cross'
- NAME = 'cross marker'
+ ICON = "add-shape-cross"
+ NAME = "cross marker"
SHORT_NAME = "cross"
"""Metadata for this kind of ROI"""
@@ -177,17 +173,14 @@ class CrossROI(HandleBasedROI, items.LineMixIn):
pos = points[0]
self.setPosition(pos)
- def getPosition(self):
- """Returns the position of this ROI
-
- :rtype: numpy.ndarray
- """
+ def getPosition(self) -> Tuple[float, float]:
+ """Returns the position of this ROI"""
return self._handle.getPosition()
- def setPosition(self, pos):
+ def setPosition(self, pos: Tuple[float, float]):
"""Set the position of this ROI
- :param numpy.ndarray pos: 2d-coordinate of this point
+ :param pos: 2d-coordinate of this point
"""
self._handle.setPosition(*pos)
@@ -213,8 +206,8 @@ class LineROI(HandleBasedROI, items.LineMixIn):
in the center to translate the full ROI.
"""
- ICON = 'add-shape-diagonal'
- NAME = 'line ROI'
+ ICON = "add-shape-diagonal"
+ NAME = "line ROI"
SHORT_NAME = "line"
"""Metadata for this kind of ROI"""
@@ -244,11 +237,12 @@ class LineROI(HandleBasedROI, items.LineMixIn):
self._updateItemProperty(event, self, self.__shape)
super(LineROI, self)._updated(event, checkVisibility)
- def _updatedStyle(self, event, style):
+ def _updatedStyle(self, event, style: items.CurveStyle):
super(LineROI, self)._updatedStyle(event, style)
self.__shape.setColor(style.getColor())
self.__shape.setLineStyle(style.getLineStyle())
self.__shape.setLineWidth(style.getLineWidth())
+ self.__shape.setLineGapColor(style.getLineGapColor())
def setFirstShapePoints(self, points):
assert len(points) == 2
@@ -257,7 +251,7 @@ class LineROI(HandleBasedROI, items.LineMixIn):
def _updateText(self, text):
self._handleLabel.setText(text)
- def setEndPoints(self, startPoint, endPoint):
+ def setEndPoints(self, startPoint: numpy.ndarray, endPoint: numpy.ndarray):
"""Set this line location using the ending points
:param numpy.ndarray startPoint: Staring bounding point of the line
@@ -266,7 +260,7 @@ class LineROI(HandleBasedROI, items.LineMixIn):
if not numpy.array_equal((startPoint, endPoint), self.getEndPoints()):
self.__updateEndPoints(startPoint, endPoint)
- def __updateEndPoints(self, startPoint, endPoint):
+ def __updateEndPoints(self, startPoint: numpy.ndarray, endPoint: numpy.ndarray):
"""Update marker and shape to match given end points
:param numpy.ndarray startPoint: Staring bounding point of the line
@@ -328,28 +322,44 @@ class LineROI(HandleBasedROI, items.LineMixIn):
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)
+ 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
+ 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'
+ ICON = "add-shape-horizontal"
+ NAME = "horizontal line ROI"
SHORT_NAME = "hline"
"""Metadata for this kind of ROI"""
@@ -366,16 +376,15 @@ class HorizontalLineROI(RegionOfInterest, items.LineMixIn):
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:
+ if event == items.ItemChangedType.EDITABLE:
self._marker._setDraggable(self.isEditable())
- elif event in [items.ItemChangedType.VISIBLE,
- items.ItemChangedType.SELECTABLE]:
+ elif event in [items.ItemChangedType.VISIBLE, items.ItemChangedType.SELECTABLE]:
self._updateItemProperty(event, self, self._marker)
super(HorizontalLineROI, self)._updated(event, checkVisibility)
+ def _updateText(self, text: str):
+ self._marker.setText(text)
+
def _updatedStyle(self, event, style):
self._marker.setColor(style.getColor())
self._marker.setLineStyle(style.getLineStyle())
@@ -387,18 +396,15 @@ class HorizontalLineROI(RegionOfInterest, items.LineMixIn):
return
self.setPosition(pos)
- def getPosition(self):
- """Returns the position of this line if the horizontal axis
-
- :rtype: float
- """
+ def getPosition(self) -> float:
+ """Returns the position of this line if the horizontal axis"""
pos = self._marker.getPosition()
return pos[1]
- def setPosition(self, pos):
+ def setPosition(self, pos: float):
"""Set the position of this ROI
- :param float pos: Horizontal position of this line
+ :param pos: Horizontal position of this line
"""
self._marker.setPosition(0, pos)
@@ -412,15 +418,15 @@ class HorizontalLineROI(RegionOfInterest, items.LineMixIn):
self.sigRegionChanged.emit()
def __str__(self):
- params = 'y: %f' % self.getPosition()
+ 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'
+ ICON = "add-shape-vertical"
+ NAME = "vertical line ROI"
SHORT_NAME = "vline"
"""Metadata for this kind of ROI"""
@@ -437,16 +443,15 @@ class VerticalLineROI(RegionOfInterest, items.LineMixIn):
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:
+ if event == items.ItemChangedType.EDITABLE:
self._marker._setDraggable(self.isEditable())
- elif event in [items.ItemChangedType.VISIBLE,
- items.ItemChangedType.SELECTABLE]:
+ elif event in [items.ItemChangedType.VISIBLE, items.ItemChangedType.SELECTABLE]:
self._updateItemProperty(event, self, self._marker)
super(VerticalLineROI, self)._updated(event, checkVisibility)
+ def _updateText(self, text: str):
+ self._marker.setText(text)
+
def _updatedStyle(self, event, style):
self._marker.setColor(style.getColor())
self._marker.setLineStyle(style.getLineStyle())
@@ -456,15 +461,12 @@ class VerticalLineROI(RegionOfInterest, items.LineMixIn):
pos = points[0, 0]
self.setPosition(pos)
- def getPosition(self):
- """Returns the position of this line if the horizontal axis
-
- :rtype: float
- """
+ def getPosition(self) -> float:
+ """Returns the position of this line if the horizontal axis"""
pos = self._marker.getPosition()
return pos[0]
- def setPosition(self, pos):
+ def setPosition(self, pos: float):
"""Set the position of this ROI
:param float pos: Horizontal position of this line
@@ -481,7 +483,7 @@ class VerticalLineROI(RegionOfInterest, items.LineMixIn):
self.sigRegionChanged.emit()
def __str__(self):
- params = 'x: %f' % self.getPosition()
+ params = "x: %f" % self.getPosition()
return "%s(%s)" % (self.__class__.__name__, params)
@@ -492,8 +494,8 @@ class RectangleROI(HandleBasedROI, items.LineMixIn):
center to translate the full ROI.
"""
- ICON = 'add-shape-rectangle'
- NAME = 'rectangle ROI'
+ ICON = "add-shape-rectangle"
+ NAME = "rectangle ROI"
SHORT_NAME = "rectangle"
"""Metadata for this kind of ROI"""
@@ -530,6 +532,7 @@ class RectangleROI(HandleBasedROI, items.LineMixIn):
self.__shape.setColor(style.getColor())
self.__shape.setLineStyle(style.getLineStyle())
self.__shape.setLineWidth(style.getLineWidth())
+ self.__shape.setLineGapColor(style.getLineGapColor())
def setFirstShapePoints(self, points):
assert len(points) == 2
@@ -598,11 +601,12 @@ class RectangleROI(HandleBasedROI, items.LineMixIn):
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())):
+ """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)
@@ -661,17 +665,38 @@ class RectangleROI(HandleBasedROI, items.LineMixIn):
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._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._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)
@@ -679,7 +704,7 @@ class RectangleROI(HandleBasedROI, items.LineMixIn):
origin = self.getOrigin()
w, h = self.getSize()
params = origin[0], origin[1], w, h
- params = 'origin: %f %f; width: %f; height: %f' % params
+ params = "origin: %f %f; width: %f; height: %f" % params
return "%s(%s)" % (self.__class__.__name__, params)
@@ -690,8 +715,8 @@ class CircleROI(HandleBasedROI, items.LineMixIn):
and one anchor on the perimeter to change the radius.
"""
- ICON = 'add-shape-circle'
- NAME = 'circle ROI'
+ ICON = "add-shape-circle"
+ NAME = "circle ROI"
SHORT_NAME = "circle"
"""Metadata for this kind of ROI"""
@@ -731,6 +756,7 @@ class CircleROI(HandleBasedROI, items.LineMixIn):
self.__shape.setColor(style.getColor())
self.__shape.setLineStyle(style.getLineStyle())
self.__shape.setLineWidth(style.getLineWidth())
+ self.__shape.setLineGapColor(style.getLineGapColor())
def setFirstShapePoints(self, points):
assert len(points) == 2
@@ -779,8 +805,7 @@ class CircleROI(HandleBasedROI, items.LineMixIn):
self._updateGeometry()
def setGeometry(self, center, radius):
- """Set the geometry of the ROI
- """
+ """Set the geometry of the ROI"""
if numpy.array_equal(center, self.getCenter()):
self.setRadius(radius)
else:
@@ -797,8 +822,9 @@ class CircleROI(HandleBasedROI, items.LineMixIn):
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 = numpy.array(
+ (numpy.cos(angles) * self.__radius, numpy.sin(angles) * self.__radius)
+ ).T
circleShape += center
self.__shape.setPoints(circleShape)
self.sigRegionChanged.emit()
@@ -821,7 +847,7 @@ class CircleROI(HandleBasedROI, items.LineMixIn):
center = self.getCenter()
radius = self.getRadius()
params = center[0], center[1], radius
- params = 'center: %f %f; radius: %f;' % params
+ params = "center: %f %f; radius: %f;" % params
return "%s(%s)" % (self.__class__.__name__, params)
@@ -833,8 +859,8 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
minor-radius. These two anchors also allow to change the orientation.
"""
- ICON = 'add-shape-ellipse'
- NAME = 'ellipse ROI'
+ ICON = "add-shape-ellipse"
+ NAME = "ellipse ROI"
SHORT_NAME = "ellipse"
"""Metadata for this kind of ROI"""
@@ -860,8 +886,10 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
self.__shape = shape
self.addItem(shape)
- self._radius = 0., 0.
- self._orientation = 0. # angle in radians between the X-axis and the _handleAxis0
+ self._radius = 0.0, 0.0
+ self._orientation = (
+ 0.0 # angle in radians between the X-axis and the _handleAxis0
+ )
def _updated(self, event=None, checkVisibility=True):
if event == items.ItemChangedType.VISIBLE:
@@ -873,6 +901,7 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
self.__shape.setColor(style.getColor())
self.__shape.setLineStyle(style.getLineStyle())
self.__shape.setLineWidth(style.getLineWidth())
+ self.__shape.setLineGapColor(style.getLineGapColor())
def setFirstShapePoints(self, points):
assert len(points) == 2
@@ -905,9 +934,9 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
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)
+ self.setGeometry(
+ center=center, radius=(radius, radius), orientation=orientation
+ )
def _updateText(self, text):
self._handleLabel.setText(text)
@@ -1007,10 +1036,11 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
# 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):
-
+ if (
+ numpy.array_equal(center, self.getCenter())
+ or radius != self._radius
+ or orientation != self._orientation
+ ):
# Update parameters directly
self._radius = radius
self._orientation = orientation
@@ -1030,10 +1060,18 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
# _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)])
+ 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):
@@ -1043,10 +1081,12 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
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))
+ 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
@@ -1083,8 +1123,10 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
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
+ 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()
@@ -1092,7 +1134,10 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
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
+ params = (
+ "center: %f %f; major radius: %f: minor radius: %f; orientation: %f"
+ % params
+ )
return "%s(%s)" % (self.__class__.__name__, params)
@@ -1102,8 +1147,8 @@ class PolygonROI(HandleBasedROI, items.LineMixIn):
This ROI provides 1 anchor for each point of the polygon.
"""
- ICON = 'add-shape-polygon'
- NAME = 'polygon ROI'
+ ICON = "add-shape-polygon"
+ NAME = "polygon ROI"
SHORT_NAME = "polygon"
"""Metadata for this kind of ROI"""
@@ -1134,6 +1179,7 @@ class PolygonROI(HandleBasedROI, items.LineMixIn):
self.__shape.setColor(style.getColor())
self.__shape.setLineStyle(style.getLineStyle())
self.__shape.setLineWidth(style.getLineWidth())
+ self.__shape.setLineGapColor(style.getLineGapColor())
if self._handleClose is not None:
color = self._computeHandleColor(style.getColor())
self._handleClose.setColor(color)
@@ -1156,8 +1202,7 @@ class PolygonROI(HandleBasedROI, items.LineMixIn):
self.setPoints(points)
def creationStarted(self):
- """"Called when the ROI creation interaction was started.
- """
+ """Called when the ROI creation interaction was started."""
# Handle to see where to close the polygon
self._handleClose = self.addUserHandle()
self._handleClose.setSymbol("o")
@@ -1178,8 +1223,7 @@ class PolygonROI(HandleBasedROI, items.LineMixIn):
return self._handleClose is not None
def creationFinalized(self):
- """"Called when the ROI creation interaction was finalized.
- """
+ """Called when the ROI creation interaction was finalized."""
self.removeHandle(self._handleClose)
self._handleClose = None
self.removeItem(self.__shape)
@@ -1206,7 +1250,7 @@ class PolygonROI(HandleBasedROI, items.LineMixIn):
:param numpy.ndarray pos: 2d-coordinate of this point
"""
- assert(len(points.shape) == 2 and points.shape[1] == 2)
+ assert len(points.shape) == 2 and points.shape[1] == 2
if numpy.array_equal(points, self._points):
return # Nothing has changed
@@ -1277,7 +1321,7 @@ class PolygonROI(HandleBasedROI, items.LineMixIn):
def __str__(self):
points = self._points
- params = '; '.join('%f %f' % (pt[0], pt[1]) for pt in points)
+ params = "; ".join("%f %f" % (pt[0], pt[1]) for pt in points)
return "%s(%s)" % (self.__class__.__name__, params)
@docstring(HandleBasedROI)
@@ -1300,8 +1344,8 @@ class PolygonROI(HandleBasedROI, items.LineMixIn):
class HorizontalRangeROI(RegionOfInterest, items.LineMixIn):
"""A ROI identifying an horizontal range in a 1D plot."""
- ICON = 'add-range-horizontal'
- NAME = 'horizontal range ROI'
+ ICON = "add-range-horizontal"
+ NAME = "horizontal range ROI"
SHORT_NAME = "hrange"
_plotShape = "line"
@@ -1333,16 +1377,13 @@ class HorizontalRangeROI(RegionOfInterest, items.LineMixIn):
self._updatePos(vmin, vmax)
def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.NAME:
- self._updateText()
- elif event == items.ItemChangedType.EDITABLE:
+ if event == items.ItemChangedType.EDITABLE:
self._updateEditable()
- self._updateText()
+ self._updateText(self.getText())
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]:
+ 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)
@@ -1353,8 +1394,7 @@ class HorizontalRangeROI(RegionOfInterest, items.LineMixIn):
m.setColor(style.getColor())
m.setLineWidth(style.getLineWidth())
- def _updateText(self):
- text = self.getName()
+ def _updateText(self, text: str):
if self.isEditable():
self._markerMin.setText("")
self._markerCen.setText(text)
@@ -1409,8 +1449,10 @@ class HorizontalRangeROI(RegionOfInterest, items.LineMixIn):
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)
+ err = (
+ "Can't set vmin and vmax because vmin >= vmax "
+ "vmin = %s, vmax = %s" % (vmin, vmax)
+ )
raise ValueError(err)
self._updatePos(vmin, vmax)
@@ -1515,5 +1557,5 @@ class HorizontalRangeROI(RegionOfInterest, items.LineMixIn):
def __str__(self):
vrange = self.getRange()
- params = 'min: %f; max: %f' % vrange
+ params = "min: %f; max: %f" % vrange
return "%s(%s)" % (self.__class__.__name__, params)
diff --git a/src/silx/gui/plot/items/scatter.py b/src/silx/gui/plot/items/scatter.py
index 96fb311..c46b60c 100644
--- a/src/silx/gui/plot/items/scatter.py
+++ b/src/silx/gui/plot/items/scatter.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2022 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -33,6 +33,7 @@ from collections import namedtuple
import logging
import threading
import numpy
+from matplotlib.tri import LinearTriInterpolator, Triangulation
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, CancelledError
@@ -41,7 +42,6 @@ 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
@@ -51,8 +51,7 @@ _logger = logging.getLogger(__name__)
class _GreedyThreadPoolExecutor(ThreadPoolExecutor):
- """:class:`ThreadPoolExecutor` with an extra :meth:`submit_greedy` method.
- """
+ """:class:`ThreadPoolExecutor` with an extra :meth:`submit_greedy` method."""
def __init__(self, *args, **kwargs):
super(_GreedyThreadPoolExecutor, self).__init__(*args, **kwargs)
@@ -76,8 +75,7 @@ class _GreedyThreadPoolExecutor(ThreadPoolExecutor):
if not future.done():
future.cancel()
- future = super(_GreedyThreadPoolExecutor, self).submit(
- fn, *args, **kwargs)
+ future = super(_GreedyThreadPoolExecutor, self).submit(fn, *args, **kwargs)
self.__futures[queue].append(future)
return future
@@ -85,6 +83,7 @@ class _GreedyThreadPoolExecutor(ThreadPoolExecutor):
# 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.
@@ -97,7 +96,7 @@ def _get_z_line_length(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
+ beginnings = numpy.where(sign == -sign[0])[0] + 1
if len(beginnings) == 0:
return 0
length = beginnings[0]
@@ -121,11 +120,11 @@ def _guess_z_grid_shape(x, y):
"""
width = _get_z_line_length(x)
if width != 0:
- return 'row', (int(numpy.ceil(len(x) / width)), width)
+ 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 "column", (height, int(numpy.ceil(len(y) / height)))
return None
@@ -139,7 +138,7 @@ def is_monotonic(array):
:rtype: int
"""
diff = numpy.diff(numpy.ravel(array))
- with numpy.errstate(invalid='ignore'):
+ with numpy.errstate(invalid="ignore"):
if numpy.all(diff >= 0):
return 1
elif numpy.all(diff <= 0):
@@ -168,7 +167,7 @@ def _guess_grid(x, y):
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
+ 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)
@@ -211,18 +210,24 @@ def _quadrilateral_grid_coords(points):
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)
+ 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], 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]
+ 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]
@@ -259,11 +264,13 @@ def _quadrilateral_grid_as_triangles(points):
_RegularGridInfo = namedtuple(
- '_RegularGridInfo', ['bounds', 'origin', 'scale', 'shape', 'order'])
+ "_RegularGridInfo", ["bounds", "origin", "scale", "shape", "order"]
+)
_HistogramInfo = namedtuple(
- '_HistogramInfo', ['mean', 'count', 'sum', 'origin', 'scale', 'shape'])
+ "_HistogramInfo", ["mean", "count", "sum", "origin", "scale", "shape"]
+)
class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
@@ -278,7 +285,7 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
ScatterVisualizationMixIn.Visualization.REGULAR_GRID,
ScatterVisualizationMixIn.Visualization.IRREGULAR_GRID,
ScatterVisualizationMixIn.Visualization.BINNED_STATISTIC,
- )
+ )
"""Overrides supported Visualizations"""
def __init__(self):
@@ -288,7 +295,7 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
self._value = ()
self.__alpha = None
# Cache Delaunay triangulation future object
- self.__delaunayFuture = None
+ self.__triangulationFuture = None
# Cache interpolator future object
self.__interpolatorFuture = None
self.__executor = None
@@ -310,7 +317,9 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
data = getattr(
histoInfo,
self.getVisualizationParameter(
- self.VisualizationParameter.BINNED_STATISTIC_FUNCTION))
+ self.VisualizationParameter.BINNED_STATISTIC_FUNCTION
+ ),
+ )
else:
data = self.getValueData(copy=False)
self._setColormappedData(data, copy=False)
@@ -319,8 +328,9 @@ class Scatter(PointsBase, ColormapMixIn, 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)):
+ if bool(mode is self.Visualization.BINNED_STATISTIC) ^ bool(
+ previous is self.Visualization.BINNED_STATISTIC
+ ):
self._updateColormappedData()
return True
else:
@@ -331,16 +341,22 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
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):
+ 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):
+ 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()
@@ -351,14 +367,16 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
@docstring(ScatterVisualizationMixIn)
def getCurrentVisualizationParameter(self, parameter):
value = self.getVisualizationParameter(parameter)
- if (parameter is self.VisualizationParameter.DATA_BOUNDS_HINT or
- value is not None):
+ 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
@@ -378,15 +396,19 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
"""Get grid info"""
if self.__cacheRegularGridInfo is None:
shape = self.getVisualizationParameter(
- self.VisualizationParameter.GRID_SHAPE)
+ self.VisualizationParameter.GRID_SHAPE
+ )
order = self.getVisualizationParameter(
- self.VisualizationParameter.GRID_MAJOR_ORDER)
+ self.VisualizationParameter.GRID_MAJOR_ORDER
+ )
if shape is None or order is None:
- guess = _guess_grid(self.getXData(copy=False),
- self.getYData(copy=False))
+ 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')
+ "Cannot guess a grid: Cannot display as regular grid image"
+ )
return None
if shape is None:
shape = guess[1]
@@ -397,16 +419,18 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
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")
+ "More data points than provided grid shape size: extends grid"
+ )
dim0, dim1 = shape
- if order == 'row': # keep dim1, enlarge dim0
+ 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)
+ self.VisualizationParameter.GRID_BOUNDS
+ )
if bounds is None:
x, y = self.getXData(copy=False), self.getYData(copy=False)
min_, max_ = min_max(x)
@@ -416,10 +440,12 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
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))
+ 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.
+ scale = 1.0, 1.0
elif scale[0] == 0:
scale = scale[1], scale[1]
elif scale[1] == 0:
@@ -428,7 +454,8 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
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)
+ bounds=bounds, origin=origin, scale=scale, shape=shape, order=order
+ )
return self.__cacheRegularGridInfo
@@ -436,9 +463,10 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
"""Get histogram info"""
if self.__cacheHistogramInfo is None:
shape = self.getVisualizationParameter(
- self.VisualizationParameter.BINNED_STATISTIC_SHAPE)
+ self.VisualizationParameter.BINNED_STATISTIC_SHAPE
+ )
if shape is None:
- shape = 100, 100 # TODO compute auto shape
+ shape = 100, 100 # TODO compute auto shape
x, y, values = self.getData(copy=False)[:3]
if len(x) == 0: # No histogram
@@ -451,31 +479,40 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
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)))
+ ranges = (tuple(min_max(y, finite=True)), tuple(min_max(x, finite=True)))
rangesHint = self.getVisualizationParameter(
- self.VisualizationParameter.DATA_BOUNDS_HINT)
+ 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))
+ 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)
+ 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))
+ scale = (
+ (xEdges[-1] - xEdges[0]) / (len(xEdges) - 1),
+ (yEdges[-1] - yEdges[0]) / (len(yEdges) - 1),
+ )
- with numpy.errstate(divide='ignore', invalid='ignore'):
+ 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)
+ mean=histo,
+ count=counts,
+ sum=sums,
+ origin=origin,
+ scale=scale,
+ shape=shape,
+ )
return self.__cacheHistogramInfo
@@ -495,7 +532,8 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
"""Update backend renderer"""
# Filter-out values <= 0
xFiltered, yFiltered, valueFiltered, xerror, yerror = self.getData(
- copy=False, displayed=True)
+ 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))
@@ -509,62 +547,79 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
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):
+ 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))
+ 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())
+ alpha=self.getAlpha(),
+ )
elif visualization is self.Visualization.POINTS:
rgbacolors = self.__applyColormapToData()
- 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)
+ return backend.addCurve(
+ xFiltered,
+ yFiltered,
+ color=rgbacolors[mask],
+ gapcolor=None,
+ 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):
+ 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:
+ try:
+ triangulation = self._getTriangulationFuture().result()
+ except (RuntimeError, ValueError):
_logger.warning(
- 'Cannot get a triangulation: Cannot display as solid surface')
+ "Cannot get a triangulation: Cannot display as solid surface"
+ )
return None
else:
rgbacolors = self.__applyColormapToData()
- triangles = triangulation.simplices.astype(numpy.int32)
- return backend.addTriangles(xFiltered,
- yFiltered,
- triangles,
- color=rgbacolors[mask],
- alpha=self.getAlpha())
+ triangles = triangulation.triangles.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()
@@ -572,7 +627,7 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
return None
dim0, dim1 = gridInfo.shape
- if gridInfo.order == 'column': # transposition needed
+ if gridInfo.order == "column": # transposition needed
dim0, dim1 = dim1, dim0
values = self.getValueData(copy=False)
@@ -580,20 +635,21 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
image = values.reshape(dim0, dim1)
else:
# The points do not fill the whole image
- if (self.__alpha is None and
- numpy.issubdtype(values.dtype, numpy.floating)):
+ if self.__alpha is None and numpy.issubdtype(
+ values.dtype, numpy.floating
+ ):
image = numpy.empty(dim0 * dim1, dtype=values.dtype)
- image[:len(values)] = values
- image[len(values):] = float('nan') # Transparent pixels
+ image[: len(values)] = values
+ image[len(values) :] = float("nan") # Transparent pixels
image.shape = dim0, dim1
else: # Per value alpha or no NaN, so convert to RGBA
rgbacolors = self.__applyColormapToData()
image = numpy.empty((dim0 * dim1, 4), dtype=numpy.uint8)
- image[:len(rgbacolors)] = rgbacolors
- image[len(rgbacolors):] = (0, 0, 0, 0) # Transparent pixels
+ image[: len(rgbacolors)] = rgbacolors
+ image[len(rgbacolors) :] = (0, 0, 0, 0) # Transparent pixels
image.shape = dim0, dim1, 4
- if gridInfo.order == 'column':
+ if gridInfo.order == "column":
if image.ndim == 2:
image = numpy.transpose(image)
else:
@@ -613,7 +669,8 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
origin=gridInfo.origin,
scale=gridInfo.scale,
colormap=colormap,
- alpha=self.getAlpha())
+ alpha=self.getAlpha(),
+ )
elif visualization is self.Visualization.IRREGULAR_GRID:
gridInfo = self.__getRegularGridInfo()
@@ -629,33 +686,37 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
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)
+ return backend.addCurve(
+ xFiltered,
+ yFiltered,
+ color=rgbacolors[mask],
+ gapcolor=None,
+ 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':
+ if gridOrder == "row":
shape = int(numpy.ceil(nbpoints / shape[1])), shape[1]
- else: # column-major order
+ 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'
+ gridOrder = "row" if shape[0] == 1 else "column"
- if gridOrder == 'row':
+ if gridOrder == "row":
points[0, :, 0] = xFiltered
points[0, :, 1] = yFiltered
else: # column-major order
@@ -663,35 +724,51 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
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[1, :-1] = (
+ points[0, :-1]
+ + numpy.cross(points[0, 1:] - points[0, :-1], (0.0, 0.0, 1.0))[
+ :, :2
+ ]
+ )
+ points[1, -1] = (
+ points[0, -1]
+ + numpy.cross(points[0, -1] - points[0, -2], (0.0, 0.0, 1.0))[
+ :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
+ elif gridOrder == "row": # row-major order
if nbpoints != numpy.prod(shape):
- points = numpy.empty((numpy.prod(shape), 2), dtype=numpy.float64)
+ 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:, 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
+ else: # column-major order
if nbpoints != numpy.prod(shape):
- points = numpy.empty((numpy.prod(shape), 2), dtype=numpy.float64)
+ 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:, 0] = yFiltered[
+ index - (numpy.prod(shape) - nbpoints) : index
+ ]
points[nbpoints:, 1] = xFiltered[-1]
else:
points = numpy.transpose((yFiltered, xFiltered))
@@ -700,25 +777,24 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
coords, indices = _quadrilateral_grid_as_triangles(points)
# Remove unused extra triangles
- coords = coords[:4*nbpoints]
- indices = indices[:2*nbpoints]
+ coords = coords[: 4 * nbpoints]
+ indices = indices[: 2 * nbpoints]
- if gridOrder == 'row':
+ 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)
+ (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())
+ return backend.addTriangles(
+ x, y, indices, color=gridcolors, alpha=self.getAlpha()
+ )
else:
_logger.error("Unhandled visualization %s", visualization)
@@ -747,11 +823,13 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
if gridInfo is None:
return None
- if gridInfo.order == 'row':
+ 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
+ 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,))
@@ -768,9 +846,16 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
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]
+ 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
@@ -784,69 +869,43 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
self.__executor = _GreedyThreadPoolExecutor(max_workers=2)
return self.__executor
- def _getDelaunay(self):
- """Returns a :class:`Future` which result is the Delaunay object.
+ def _getTriangulationFuture(self):
+ """Returns a :class:`Future` which result is the Triangulation object.
:rtype: concurrent.futures.Future
"""
- if self.__delaunayFuture is None or self.__delaunayFuture.cancelled():
+ if self.__triangulationFuture is None or self.__triangulationFuture.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])
+ self.__triangulationFuture = self.__getExecutor().submit_greedy(
+ "Triangulation", Triangulation, x[mask], y[mask]
+ )
- return self.__delaunayFuture
+ return self.__triangulationFuture
@staticmethod
- def __initInterpolator(delaunayFuture, values):
+ def __initInterpolator(triangulationFuture, values):
"""Returns an interpolator for the given data points
- :param concurrent.futures.Future delaunayFuture:
- Future object which result is a Delaunay object
+ :param concurrent.futures.Future triangulationFuture:
+ Future object which result is a Triangulation object
:param numpy.ndarray values: The data value of valid points.
:rtype: Union[callable,None]
"""
- # Wait for Delaunay to complete
+ # Wait for Triangulation to complete
try:
- triangulation = delaunayFuture.result()
+ triangulation = triangulationFuture.result()
+ except (RuntimeError, ValueError):
+ return None # triangulation failed
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 None
- return interpolator
+ return LinearTriInterpolator(triangulation, values)
- def _getInterpolator(self):
+ def _getInterpolatorFuture(self):
"""Returns a :class:`Future` which result is the interpolator.
The interpolator is a callable taking an array Nx2 of points
@@ -856,8 +915,7 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
:rtype: concurrent.futures.Future
"""
- if (self.__interpolatorFuture is None or
- self.__interpolatorFuture.cancelled()):
+ 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
@@ -865,8 +923,11 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
x, y, values = x[mask], y[mask], values[mask]
self.__interpolatorFuture = self.__getExecutor().submit_greedy(
- 'interpolator',
- self.__initInterpolator, self._getDelaunay(), values)
+ "interpolator",
+ self.__initInterpolator,
+ self._getTriangulationFuture(),
+ values,
+ )
return self.__interpolatorFuture
def _logFilterData(self, xPositive, yPositive):
@@ -928,11 +989,13 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
assert len(data) == 5
return data
- return (self.getXData(copy),
- self.getYData(copy),
- self.getValueData(copy),
- self.getXErrorData(copy),
- self.getYErrorData(copy))
+ 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):
@@ -951,7 +1014,7 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
: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
+ :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.
"""
@@ -961,14 +1024,13 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
# Convert complex data
if numpy.iscomplexobj(value):
- _logger.warning(
- 'Converting value data to absolute value to plot it.')
+ _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.__triangulationFuture is not None:
+ self.__triangulationFuture.cancel()
+ self.__triangulationFuture = None
if self.__interpolatorFuture is not None:
self.__interpolatorFuture.cancel()
self.__interpolatorFuture = None
@@ -984,10 +1046,10 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
alpha = numpy.array(alpha, copy=copy)
assert alpha.ndim == 1
assert len(x) == len(alpha)
- if alpha.dtype.kind != 'f':
+ 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.)
+ if numpy.any(numpy.logical_or(alpha < 0.0, alpha > 1.0)):
+ alpha = numpy.clip(alpha, 0.0, 1.0)
self.__alpha = alpha
# set x, y, xerror, yerror
diff --git a/src/silx/gui/plot/items/shape.py b/src/silx/gui/plot/items/shape.py
index dc35864..c911924 100644
--- a/src/silx/gui/plot/items/shape.py
+++ b/src/silx/gui/plot/items/shape.py
@@ -33,11 +33,18 @@ import logging
import numpy
-from ... import colors
-from ..utils.intersections import lines_intersection
from .core import (
- Item, DataItem,
- AlphaMixIn, ColorMixIn, FillMixIn, ItemChangedType, ItemMixInBase, LineMixIn, YAxisMixIn)
+ Item,
+ DataItem,
+ AlphaMixIn,
+ ColorMixIn,
+ FillMixIn,
+ ItemChangedType,
+ LineMixIn,
+ LineGapColorMixIn,
+ YAxisMixIn,
+)
+from ....utils.deprecation import deprecated
_logger = logging.getLogger(__name__)
@@ -65,41 +72,20 @@ class _OverlayItem(Item):
self._updated(ItemChangedType.OVERLAY)
-class _TwoColorsLineMixIn(LineMixIn):
+class _TwoColorsLineMixIn(LineMixIn, LineGapColorMixIn):
"""Mix-in class for items with a background color for dashes"""
def __init__(self):
LineMixIn.__init__(self)
- self.__backgroundColor = None
+ LineGapColorMixIn.__init__(self)
+ @deprecated(replacement="getLineGapColor", since_version="2.0.0")
def getLineBgColor(self):
- """Returns the RGBA background color of dash line
+ return self.getLineGapColor()
- :rtype: 4-tuple of float in [0, 1] or array of colors
- """
- return self.__backgroundColor
-
- def setLineBgColor(self, color, copy: bool=True):
- """Set dash line background 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 copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- """
- if color is not None:
- if isinstance(color, str):
- 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.__backgroundColor = color
+ @deprecated(replacement="setLineGapColor", since_version="2.0.0")
+ def setLineBgColor(self, color, copy: bool = True):
+ self.setLineGapColor(color)
self._updated(ItemChangedType.LINE_BG_COLOR)
@@ -117,7 +103,7 @@ class Shape(_OverlayItem, ColorMixIn, FillMixIn, _TwoColorsLineMixIn):
ColorMixIn.__init__(self)
FillMixIn.__init__(self)
_TwoColorsLineMixIn.__init__(self)
- assert type_ in ('hline', 'polygon', 'rectangle', 'vline', 'polylines')
+ assert type_ in ("hline", "polygon", "rectangle", "vline", "polylines")
self._type = type_
self._points = ()
self._handle = None
@@ -126,15 +112,17 @@ class Shape(_OverlayItem, ColorMixIn, FillMixIn, _TwoColorsLineMixIn):
"""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())
+ return backend.addShape(
+ x,
+ y,
+ shape=self.getType(),
+ color=self.getColor(),
+ fill=self.isFill(),
+ overlay=self.isOverlay(),
+ linestyle=self.getLineStyle(),
+ linewidth=self.getLineWidth(),
+ gapcolor=self.getLineGapColor(),
+ )
def getType(self):
"""Returns the type of shape to draw.
@@ -226,11 +214,11 @@ class _BaseExtent(DataItem):
:param str axis: Either 'x' or 'y'.
"""
- def __init__(self, axis='x'):
- assert axis in ('x', 'y')
+ def __init__(self, axis="x"):
+ assert axis in ("x", "y")
DataItem.__init__(self)
self.__axis = axis
- self.__range = 1., 100.
+ self.__range = 1.0, 100.0
def setRange(self, min_, max_):
"""Set the range of the extent of this item in data coordinates.
@@ -262,17 +250,17 @@ class _BaseExtent(DataItem):
plot = self.getPlot()
if plot is not None:
- axis = plot.getXAxis() if self.__axis == 'x' else plot.getYAxis()
+ 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')
+ if self.__axis == "x":
+ return min_, max_, float("nan"), float("nan")
else:
- return float('nan'), float('nan'), min_, max_
+ return float("nan"), float("nan"), min_, max_
class XAxisExtent(_BaseExtent):
@@ -282,8 +270,9 @@ class XAxisExtent(_BaseExtent):
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')
+ _BaseExtent.__init__(self, axis="x")
class YAxisExtent(_BaseExtent, YAxisMixIn):
@@ -295,7 +284,7 @@ class YAxisExtent(_BaseExtent, YAxisMixIn):
"""
def __init__(self):
- _BaseExtent.__init__(self, axis='y')
+ _BaseExtent.__init__(self, axis="y")
YAxisMixIn.__init__(self)
@@ -305,7 +294,7 @@ class Line(_OverlayItem, AlphaMixIn, ColorMixIn, _TwoColorsLineMixIn):
Warning: If slope is not finite, then the line is x = intercept.
"""
- def __init__(self, slope: float=0, intercept: float=0):
+ def __init__(self, slope: float = 0, intercept: float = 0):
assert numpy.isfinite(intercept)
_OverlayItem.__init__(self)
@@ -378,7 +367,7 @@ class Line(_OverlayItem, AlphaMixIn, ColorMixIn, _TwoColorsLineMixIn):
"""Set slope and intercept from 2 (x, y) points"""
x0, y0 = point0
x1, y1 = point1
- if x0 == x1: # Special case: vertical line
+ if x0 == x1: # Special case: vertical line
self.setSlope(float("inf"))
self.setIntercept(x0)
return
@@ -394,11 +383,11 @@ class Line(_OverlayItem, AlphaMixIn, ColorMixIn, _TwoColorsLineMixIn):
return backend.addShape(
*self.__coordinates,
- shape='polylines',
+ shape="polylines",
color=self.getColor(),
fill=False,
overlay=self.isOverlay(),
linestyle=self.getLineStyle(),
linewidth=self.getLineWidth(),
- linebgcolor=self.getLineBgColor(),
+ gapcolor=self.getLineGapColor(),
)