summaryrefslogtreecommitdiff
path: root/src/silx/gui/plot3d/items/scatter.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/silx/gui/plot3d/items/scatter.py')
-rw-r--r--src/silx/gui/plot3d/items/scatter.py224
1 files changed, 116 insertions, 108 deletions
diff --git a/src/silx/gui/plot3d/items/scatter.py b/src/silx/gui/plot3d/items/scatter.py
index c93db88..b8f2f39 100644
--- a/src/silx/gui/plot3d/items/scatter.py
+++ b/src/silx/gui/plot3d/items/scatter.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
@@ -28,16 +28,13 @@ __authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "15/11/2017"
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
+from collections import abc
import logging
+import sys
import numpy
+from matplotlib.tri import Triangulation
-from ....utils.deprecation import deprecated
from ... import _glutils as glu
-from ...plot._utils.delaunay import delaunay
from ..scene import function, primitives, utils
from ...plot.items import ScatterVisualizationMixIn
@@ -65,7 +62,8 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
noData = numpy.zeros((0, 1), dtype=numpy.float32)
symbol, size = self._getSceneSymbol()
self._scatter = primitives.Points(
- x=noData, y=noData, z=noData, value=noData, size=size)
+ x=noData, y=noData, z=noData, value=noData, size=size
+ )
self._scatter.marker = symbol
self._getScenePrimitive().children.append(self._scatter)
@@ -77,7 +75,7 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
if event in (ItemChangedType.SYMBOL, ItemChangedType.SYMBOL_SIZE):
symbol, size = self._getSceneSymbol()
self._scatter.marker = symbol
- self._scatter.setAttribute('size', size, copy=True)
+ self._scatter.setAttribute("size", size, copy=True)
super(Scatter3D, self)._updated(event)
@@ -92,10 +90,10 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
True (default) to copy the data,
False to use provided data (do not modify!)
"""
- self._scatter.setAttribute('x', x, copy=copy)
- self._scatter.setAttribute('y', y, copy=copy)
- self._scatter.setAttribute('z', z, copy=copy)
- self._scatter.setAttribute('value', value, copy=copy)
+ self._scatter.setAttribute("x", x, copy=copy)
+ self._scatter.setAttribute("y", y, copy=copy)
+ self._scatter.setAttribute("z", z, copy=copy)
+ self._scatter.setAttribute("value", value, copy=copy)
self._setColormappedData(self.getValueData(copy=False), copy=False)
self._updated(ItemChangedType.DATA)
@@ -107,10 +105,12 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
False to return internal data (do not modify!)
:return: (x, y, z, value)
"""
- return (self.getXData(copy),
- self.getYData(copy),
- self.getZData(copy),
- self.getValueData(copy))
+ return (
+ self.getXData(copy),
+ self.getYData(copy),
+ self.getZData(copy),
+ self.getValueData(copy),
+ )
def getXData(self, copy=True):
"""Returns X data coordinates.
@@ -120,7 +120,7 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
:return: X coordinates
:rtype: numpy.ndarray
"""
- return self._scatter.getAttribute('x', copy=copy).reshape(-1)
+ return self._scatter.getAttribute("x", copy=copy).reshape(-1)
def getYData(self, copy=True):
"""Returns Y data coordinates.
@@ -130,7 +130,7 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
:return: Y coordinates
:rtype: numpy.ndarray
"""
- return self._scatter.getAttribute('y', copy=copy).reshape(-1)
+ return self._scatter.getAttribute("y", copy=copy).reshape(-1)
def getZData(self, copy=True):
"""Returns Z data coordinates.
@@ -140,7 +140,7 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
:return: Z coordinates
:rtype: numpy.ndarray
"""
- return self._scatter.getAttribute('z', copy=copy).reshape(-1)
+ return self._scatter.getAttribute("z", copy=copy).reshape(-1)
def getValueData(self, copy=True):
"""Returns data values.
@@ -150,14 +150,9 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
:return: data values
:rtype: numpy.ndarray
"""
- return self._scatter.getAttribute('value', copy=copy).reshape(-1)
+ return self._scatter.getAttribute("value", copy=copy).reshape(-1)
- @deprecated(reason="Consistency with PlotWidget items",
- replacement="getValueData", since_version="0.10.0")
- def getValues(self, copy=True):
- return self.getValueData(copy)
-
- def _pickFull(self, context, threshold=0., sort='depth'):
+ def _pickFull(self, context, threshold=0.0, sort="depth"):
"""Perform picking in this item at given widget position.
:param PickContext context: Current picking context
@@ -171,9 +166,9 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
:return: Object holding the results or None
:rtype: Union[None,PickingResult]
"""
- assert sort in ('index', 'depth')
+ assert sort in ("index", "depth")
- rayNdc = context.getPickingSegment(frame='ndc')
+ rayNdc = context.getPickingSegment(frame="ndc")
if rayNdc is None: # No picking outside viewport
return None
@@ -184,49 +179,57 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
primitive = self._getScenePrimitive()
- dataPoints = numpy.transpose((xData,
- self.getYData(copy=False),
- self.getZData(copy=False),
- numpy.ones_like(xData)))
+ dataPoints = numpy.transpose(
+ (
+ xData,
+ self.getYData(copy=False),
+ self.getZData(copy=False),
+ numpy.ones_like(xData),
+ )
+ )
pointsNdc = primitive.objectToNDCTransform.transformPoints(
- dataPoints, perspectiveDivide=True)
+ dataPoints, perspectiveDivide=True
+ )
# Perform picking
distancesNdc = numpy.abs(pointsNdc[:, :2] - rayNdc[0, :2])
# TODO issue with symbol size: using pixel instead of points
threshold += self.getSymbolSize()
- thresholdNdc = 2. * threshold / numpy.array(primitive.viewport.size)
- picked = numpy.where(numpy.logical_and(
+ thresholdNdc = 2.0 * threshold / numpy.array(primitive.viewport.size)
+ picked = numpy.where(
+ numpy.logical_and(
numpy.all(distancesNdc < thresholdNdc, axis=1),
- numpy.logical_and(rayNdc[0, 2] <= pointsNdc[:, 2],
- pointsNdc[:, 2] <= rayNdc[1, 2])))[0]
+ numpy.logical_and(
+ rayNdc[0, 2] <= pointsNdc[:, 2], pointsNdc[:, 2] <= rayNdc[1, 2]
+ ),
+ )
+ )[0]
- if sort == 'depth':
+ if sort == "depth":
# Sort picked points from front to back
picked = picked[numpy.argsort(pointsNdc[picked, 2])]
if picked.size > 0:
- return PickingResult(self,
- positions=dataPoints[picked, :3],
- indices=picked,
- fetchdata=self.getValueData)
+ return PickingResult(
+ self,
+ positions=dataPoints[picked, :3],
+ indices=picked,
+ fetchdata=self.getValueData,
+ )
else:
return None
-class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
- ScatterVisualizationMixIn):
+class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn, ScatterVisualizationMixIn):
"""2D scatter data with settable visualization mode.
:param parent: The View widget this item belongs to.
"""
_VISUALIZATION_PROPERTIES = {
- ScatterVisualizationMixIn.Visualization.POINTS:
- ('symbol', 'symbolSize'),
- ScatterVisualizationMixIn.Visualization.LINES:
- ('lineWidth',),
+ ScatterVisualizationMixIn.Visualization.POINTS: ("symbol", "symbolSize"),
+ ScatterVisualizationMixIn.Visualization.LINES: ("lineWidth",),
ScatterVisualizationMixIn.Visualization.SOLID: (),
}
"""Dict {visualization mode: property names used in this mode}"""
@@ -241,7 +244,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
ScatterVisualizationMixIn.__init__(self)
self._heightMap = False
- self._lineWidth = 1.
+ self._lineWidth = 1.0
self._x = numpy.zeros((0,), dtype=numpy.float32)
self._y = numpy.zeros((0,), dtype=numpy.float32)
@@ -260,7 +263,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
for child in self._getScenePrimitive().children:
if isinstance(child, primitives.Points):
child.marker = symbol
- child.setAttribute('size', size, copy=True)
+ child.setAttribute("size", size, copy=True)
elif event is ItemChangedType.VISIBLE:
# TODO smart update?, need dirty flags
@@ -281,7 +284,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
By default, it is the current visualization mode.
:return:
"""
- assert name in ('lineWidth', 'symbol', 'symbolSize')
+ assert name in ("lineWidth", "symbol", "symbolSize")
if visualization is None:
visualization = self.getVisualization()
assert visualization in self.supportedVisualizations()
@@ -322,11 +325,11 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
:param float width: Width in pixels
"""
width = float(width)
- assert width >= 1.
+ assert width >= 1.0
if width != self._lineWidth:
self._lineWidth = width
for child in self._getScenePrimitive().children:
- if hasattr(child, 'lineWidth'):
+ if hasattr(child, "lineWidth"):
child.lineWidth = width
self._updated(ItemChangedType.LINE_WIDTH)
@@ -342,15 +345,14 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
True (default) to make a copy of the data,
False to avoid copy if possible (do not modify the arrays).
"""
- x = numpy.array(
- x, copy=copy, dtype=numpy.float32, order='C').reshape(-1)
- y = numpy.array(
- y, copy=copy, dtype=numpy.float32, order='C').reshape(-1)
+ x = numpy.array(x, copy=copy, dtype=numpy.float32, order="C").reshape(-1)
+ y = numpy.array(y, copy=copy, dtype=numpy.float32, order="C").reshape(-1)
assert len(x) == len(y)
if isinstance(value, abc.Iterable):
value = numpy.array(
- value, copy=copy, dtype=numpy.float32, order='C').reshape(-1)
+ value, copy=copy, dtype=numpy.float32, order="C"
+ ).reshape(-1)
assert len(value) == len(x)
else: # Single scalar
value = numpy.array((float(value),), dtype=numpy.float32)
@@ -376,9 +378,11 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
False to return internal data (do not modify!)
:return: (x, y, value)
"""
- return (self.getXData(copy=copy),
- self.getYData(copy=copy),
- self.getValueData(copy=copy))
+ return (
+ self.getXData(copy=copy),
+ self.getYData(copy=copy),
+ self.getValueData(copy=copy),
+ )
def getXData(self, copy=True):
"""Returns X data coordinates.
@@ -410,12 +414,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
"""
return numpy.array(self._value, copy=copy)
- @deprecated(reason="Consistency with PlotWidget items",
- replacement="getValueData", since_version="0.10.0")
- def getValues(self, copy=True):
- return self.getValueData(copy)
-
- def _pickPoints(self, context, points, threshold=1., sort='depth'):
+ def _pickPoints(self, context, points, threshold=1.0, sort="depth"):
"""Perform picking while in 'points' visualization mode
:param PickContext context: Current picking context
@@ -429,34 +428,41 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
:return: Object holding the results or None
:rtype: Union[None,PickingResult]
"""
- assert sort in ('index', 'depth')
+ assert sort in ("index", "depth")
- rayNdc = context.getPickingSegment(frame='ndc')
+ rayNdc = context.getPickingSegment(frame="ndc")
if rayNdc is None: # No picking outside viewport
return None
# Project data to NDC
primitive = self._getScenePrimitive()
pointsNdc = primitive.objectToNDCTransform.transformPoints(
- points, perspectiveDivide=True)
+ points, perspectiveDivide=True
+ )
# Perform picking
distancesNdc = numpy.abs(pointsNdc[:, :2] - rayNdc[0, :2])
thresholdNdc = threshold / numpy.array(primitive.viewport.size)
- picked = numpy.where(numpy.logical_and(
- numpy.all(distancesNdc < thresholdNdc, axis=1),
- numpy.logical_and(rayNdc[0, 2] <= pointsNdc[:, 2],
- pointsNdc[:, 2] <= rayNdc[1, 2])))[0]
+ picked = numpy.where(
+ numpy.logical_and(
+ numpy.all(distancesNdc < thresholdNdc, axis=1),
+ numpy.logical_and(
+ rayNdc[0, 2] <= pointsNdc[:, 2], pointsNdc[:, 2] <= rayNdc[1, 2]
+ ),
+ )
+ )[0]
- if sort == 'depth':
+ if sort == "depth":
# Sort picked points from front to back
picked = picked[numpy.argsort(pointsNdc[picked, 2])]
if picked.size > 0:
- return PickingResult(self,
- positions=points[picked, :3],
- indices=picked,
- fetchdata=self.getValueData)
+ return PickingResult(
+ self,
+ positions=points[picked, :3],
+ indices=picked,
+ fetchdata=self.getValueData,
+ )
else:
return None
@@ -477,7 +483,8 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
trianglesIndices = self._cachedTrianglesIndices.reshape(-1, 3)
triangles = points[trianglesIndices, :3]
selectedIndices, t, barycentric = glu.segmentTrianglesIntersection(
- rayObject, triangles)
+ rayObject, triangles
+ )
closest = numpy.argmax(barycentric, axis=1)
indices = trianglesIndices.reshape(-1, 3)[selectedIndices, closest]
@@ -488,10 +495,9 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
# Compute intersection points and get closest data point
positions = t.reshape(-1, 1) * (rayObject[1] - rayObject[0]) + rayObject[0]
- return PickingResult(self,
- positions=positions,
- indices=indices,
- fetchdata=self.getValueData)
+ return PickingResult(
+ self, positions=positions, indices=indices, fetchdata=self.getValueData
+ )
def _pickFull(self, context):
"""Perform picking in this item at given widget position.
@@ -509,22 +515,20 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
else:
zData = numpy.zeros_like(xData)
- points = numpy.transpose((xData,
- self.getYData(copy=False),
- zData,
- numpy.ones_like(xData)))
+ points = numpy.transpose(
+ (xData, self.getYData(copy=False), zData, numpy.ones_like(xData))
+ )
mode = self.getVisualization()
if mode is self.Visualization.POINTS:
# TODO issue with symbol size: using pixel instead of points
# Get "corrected" symbol size
_, threshold = self._getSceneSymbol()
- return self._pickPoints(
- context, points, threshold=max(3., threshold))
+ return self._pickPoints(context, points, threshold=max(3.0, threshold))
elif mode is self.Visualization.LINES:
# Picking only at point
- return self._pickPoints(context, points, threshold=5.)
+ return self._pickPoints(context, points, threshold=5.0)
else: # mode == 'solid'
return self._pickSolid(context, points)
@@ -543,36 +547,38 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
heightMap = self.isHeightMap()
if mode is self.Visualization.POINTS:
- z = value if heightMap else 0.
+ z = value if heightMap else 0.0
symbol, size = self._getSceneSymbol()
primitive = primitives.Points(
- x=x, y=y, z=z, value=value,
- size=size,
- colormap=self._getSceneColormap())
+ x=x, y=y, z=z, value=value, size=size, colormap=self._getSceneColormap()
+ )
primitive.marker = symbol
else:
# TODO run delaunay in a thread
# Compute lines/triangles indices if not cached
if self._cachedTrianglesIndices is None:
- triangulation = delaunay(x, y)
- if triangulation is None:
+ try:
+ triangulation = Triangulation(x, y)
+ except (RuntimeError, ValueError):
+ _logger.debug("Delaunay tesselation failed: %s", sys.exc_info()[1])
return None
self._cachedTrianglesIndices = numpy.ravel(
- triangulation.simplices.astype(numpy.uint32))
+ triangulation.triangles.astype(numpy.uint32)
+ )
- if (mode is self.Visualization.LINES and
- self._cachedLinesIndices is None):
+ if mode is self.Visualization.LINES and self._cachedLinesIndices is None:
# Compute line indices
self._cachedLinesIndices = utils.triangleToLineIndices(
- self._cachedTrianglesIndices, unicity=True)
+ self._cachedTrianglesIndices, unicity=True
+ )
if mode is self.Visualization.LINES:
indices = self._cachedLinesIndices
- renderMode = 'lines'
+ renderMode = "lines"
else:
indices = self._cachedTrianglesIndices
- renderMode = 'triangles'
+ renderMode = "triangles"
# TODO supports x, y instead of copy
if heightMap:
@@ -590,14 +596,15 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
if len(value) > 1:
value = value[indices]
triangleNormals = utils.trianglesNormal(coordinates)
- normal = numpy.empty((len(triangleNormals) * 3, 3),
- dtype=numpy.float32)
+ normal = numpy.empty(
+ (len(triangleNormals) * 3, 3), dtype=numpy.float32
+ )
normal[0::3, :] = triangleNormals
normal[1::3, :] = triangleNormals
normal[2::3, :] = triangleNormals
indices = None
else:
- normal = (0., 0., 1.)
+ normal = (0.0, 0.0, 1.0)
else:
normal = None
@@ -607,7 +614,8 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
normal=normal,
colormap=self._getSceneColormap(),
indices=indices,
- mode=renderMode)
+ mode=renderMode,
+ )
primitive.lineWidth = self.getLineWidth()
primitive.lineSmooth = False