summaryrefslogtreecommitdiff
path: root/silx/gui/plot3d/items/scatter.py
diff options
context:
space:
mode:
authorPicca Frédéric-Emmanuel <picca@debian.org>2018-03-04 10:20:27 +0100
committerPicca Frédéric-Emmanuel <picca@debian.org>2018-03-04 10:20:27 +0100
commit270d5ddc31c26b62379e3caa9044dd75ccc71847 (patch)
tree55c5bfc851dfce7172d335cd2405b214323e3caf /silx/gui/plot3d/items/scatter.py
parente19c96eff0c310c06c4f268c8b80cb33bd08996f (diff)
New upstream version 0.7.0+dfsg
Diffstat (limited to 'silx/gui/plot3d/items/scatter.py')
-rw-r--r--silx/gui/plot3d/items/scatter.py474
1 files changed, 474 insertions, 0 deletions
diff --git a/silx/gui/plot3d/items/scatter.py b/silx/gui/plot3d/items/scatter.py
new file mode 100644
index 0000000..5eea455
--- /dev/null
+++ b/silx/gui/plot3d/items/scatter.py
@@ -0,0 +1,474 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2017 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This module provides 2D and 3D scatter data item class.
+"""
+
+from __future__ import absolute_import
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "15/11/2017"
+
+import collections
+import logging
+import sys
+import numpy
+
+from ..scene import function, primitives, utils
+
+from .core import DataItem3D, Item3DChangedType, ItemChangedType
+from .mixins import ColormapMixIn, SymbolMixIn
+
+
+_logger = logging.getLevelName(__name__)
+
+
+class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
+ """Description of a 3D scatter plot.
+
+ :param parent: The View widget this item belongs to.
+ """
+
+ # TODO supports different size for each point
+
+ def __init__(self, parent=None):
+ DataItem3D.__init__(self, parent=parent)
+ ColormapMixIn.__init__(self)
+ SymbolMixIn.__init__(self)
+
+ 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)
+ self._scatter.marker = symbol
+ self._getScenePrimitive().children.append(self._scatter)
+
+ # Connect scene primitive to mix-in class
+ ColormapMixIn._setSceneColormap(self, self._scatter.colormap)
+
+ def _updated(self, event=None):
+ """Handle mix-in class updates"""
+ if event in (ItemChangedType.SYMBOL, ItemChangedType.SYMBOL_SIZE):
+ symbol, size = self._getSceneSymbol()
+ self._scatter.marker = symbol
+ self._scatter.setAttribute('size', size, copy=True)
+
+ super(Scatter3D, self)._updated(event)
+
+ def setData(self, x, y, z, value, copy=True):
+ """Set the data of the scatter plot
+
+ :param numpy.ndarray x: Array of X coordinates (single value not accepted)
+ :param y: Points Y coordinate (array-like or single value)
+ :param z: Points Z coordinate (array-like or single value)
+ :param value: Points values (array-like or single value)
+ :param bool copy:
+ 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)
+
+ ColormapMixIn._setRangeFromData(self, self.getValues(copy=False))
+ self._updated(ItemChangedType.DATA)
+
+ def getData(self, copy=True):
+ """Returns data as provided to :meth:`setData`.
+
+ :param bool copy: True to get a copy,
+ False to return internal data (do not modify!)
+ :return: (x, y, z, value)
+ """
+ return (self.getXData(copy),
+ self.getYData(copy),
+ self.getZData(copy),
+ self.getValues(copy))
+
+ def getXData(self, copy=True):
+ """Returns X data coordinates.
+
+ :param bool copy: True to get a copy,
+ False to return internal array (do not modify!)
+ :return: X coordinates
+ :rtype: numpy.ndarray
+ """
+ return self._scatter.getAttribute('x', copy=copy)
+
+ def getYData(self, copy=True):
+ """Returns Y data coordinates.
+
+ :param bool copy: True to get a copy,
+ False to return internal array (do not modify!)
+ :return: Y coordinates
+ :rtype: numpy.ndarray
+ """
+ return self._scatter.getAttribute('y', copy=copy)
+
+ def getZData(self, copy=True):
+ """Returns Z data coordinates.
+
+ :param bool copy: True to get a copy,
+ False to return internal array (do not modify!)
+ :return: Z coordinates
+ :rtype: numpy.ndarray
+ """
+ return self._scatter.getAttribute('z', copy=copy)
+
+ def getValues(self, copy=True):
+ """Returns data values.
+
+ :param bool copy: True to get a copy,
+ False to return internal array (do not modify!)
+ :return: data values
+ :rtype: numpy.ndarray
+ """
+ return self._scatter.getAttribute('value', copy=copy)
+
+
+class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn):
+ """2D scatter data with settable visualization mode.
+
+ :param parent: The View widget this item belongs to.
+ """
+
+ _VISUALIZATION_PROPERTIES = {
+ 'points': ('symbol', 'symbolSize'),
+ 'lines': ('lineWidth',),
+ 'solid': (),
+ }
+ """Dict {visualization mode: property names used in this mode}"""
+
+ def __init__(self, parent=None):
+ DataItem3D.__init__(self, parent=parent)
+ ColormapMixIn.__init__(self)
+ SymbolMixIn.__init__(self)
+
+ self._visualizationMode = 'points'
+ self._heightMap = False
+ self._lineWidth = 1.
+
+ self._x = numpy.zeros((0,), dtype=numpy.float32)
+ self._y = numpy.zeros((0,), dtype=numpy.float32)
+ self._value = numpy.zeros((0,), dtype=numpy.float32)
+
+ self._cachedLinesIndices = None
+ self._cachedTrianglesIndices = None
+
+ # Connect scene primitive to mix-in class
+ ColormapMixIn._setSceneColormap(self, function.Colormap())
+
+ def _updated(self, event=None):
+ """Handle mix-in class updates"""
+ if event in (ItemChangedType.SYMBOL, ItemChangedType.SYMBOL_SIZE):
+ symbol, size = self._getSceneSymbol()
+ for child in self._getScenePrimitive().children:
+ if isinstance(child, primitives.Points):
+ child.marker = symbol
+ child.setAttribute('size', size, copy=True)
+
+ elif event == ItemChangedType.VISIBLE:
+ # TODO smart update?, need dirty flags
+ self._updateScene()
+
+ super(Scatter2D, self)._updated(event)
+
+ def supportedVisualizations(self):
+ """Returns the list of supported visualization modes.
+
+ See :meth:`setVisualizationModes`
+
+ :rtype: tuple of str
+ """
+ return tuple(self._VISUALIZATION_PROPERTIES.keys())
+
+ def setVisualization(self, mode):
+ """Set the visualization mode of the data.
+
+ Supported visualization modes are:
+
+ - 'points': For scatter plot representation
+ - 'lines': For Delaunay tesselation-based wireframe representation
+ - 'solid': For Delaunay tesselation-based solid surface representation
+
+ :param str mode: Mode of representation to use
+ """
+ mode = str(mode)
+ assert mode in self.supportedVisualizations()
+
+ if mode != self.getVisualization():
+ self._visualizationMode = mode
+ self._updateScene()
+ self._updated(ItemChangedType.VISUALIZATION_MODE)
+
+ def getVisualization(self):
+ """Returns the current visualization mode.
+
+ See :meth:`setVisualization`
+
+ :rtype: str
+ """
+ return self._visualizationMode
+
+ def isPropertyEnabled(self, name, visualization=None):
+ """Returns true if the property is used with visualization mode.
+
+ :param str name: The name of the property to check, in:
+ 'lineWidth', 'symbol', 'symbolSize'
+ :param str visualization:
+ The visualization mode for which to get the info.
+ By default, it is the current visualization mode.
+ :return:
+ """
+ assert name in ('lineWidth', 'symbol', 'symbolSize')
+ if visualization is None:
+ visualization = self.getVisualization()
+ assert visualization in self.supportedVisualizations()
+ return name in self._VISUALIZATION_PROPERTIES[visualization]
+
+ def setHeightMap(self, heightMap):
+ """Set whether to display the data has a height map or not.
+
+ When displayed as a height map, the data values are used as
+ z coordinates.
+
+ :param bool heightMap:
+ True to display a height map,
+ False to display as 2D data with z=0
+ """
+ heightMap = bool(heightMap)
+ if heightMap != self.isHeightMap():
+ self._heightMap = heightMap
+ self._updateScene()
+ self._updated(Item3DChangedType.HEIGHT_MAP)
+
+ def isHeightMap(self):
+ """Returns True if data is displayed as a height map.
+
+ :rtype: bool
+ """
+ return self._heightMap
+
+ def getLineWidth(self):
+ """Return the curve line width in pixels (float)"""
+ return self._lineWidth
+
+ def setLineWidth(self, width):
+ """Set the width in pixel of the curve line
+
+ See :meth:`getLineWidth`.
+
+ :param float width: Width in pixels
+ """
+ width = float(width)
+ assert width >= 1.
+ if width != self._lineWidth:
+ self._lineWidth = width
+ for child in self._getScenePrimitive().children:
+ if hasattr(child, 'lineWidth'):
+ child.lineWidth = width
+ self._updated(ItemChangedType.LINE_WIDTH)
+
+ def setData(self, x, y, value, copy=True):
+ """Set the data represented by this item.
+
+ Provided arrays must have the same length.
+
+ :param numpy.ndarray x: X coordinates (array-like)
+ :param numpy.ndarray y: Y coordinates (array-like)
+ :param value: Points value: array-like or single scalar
+ :param bool copy:
+ 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)
+ assert len(x) == len(y)
+
+ if isinstance(value, collections.Iterable):
+ value = numpy.array(
+ 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)
+
+ self._x = x
+ self._y = y
+ self._value = value
+
+ # Reset cache
+ self._cachedLinesIndices = None
+ self._cachedTrianglesIndices = None
+
+ # Store data range info
+ ColormapMixIn._setRangeFromData(self, self.getValues(copy=False))
+
+ self._updateScene()
+
+ self._updated(ItemChangedType.DATA)
+
+ def getData(self, copy=True):
+ """Returns data as provided to :meth:`setData`.
+
+ :param bool copy: True to get a copy,
+ False to return internal data (do not modify!)
+ :return: (x, y, value)
+ """
+ return (self.getXData(copy=copy),
+ self.getYData(copy=copy),
+ self.getValues(copy=copy))
+
+ def getXData(self, copy=True):
+ """Returns X data coordinates.
+
+ :param bool copy: True to get a copy,
+ False to return internal array (do not modify!)
+ :return: X coordinates
+ :rtype: numpy.ndarray
+ """
+ return numpy.array(self._x, copy=copy)
+
+ def getYData(self, copy=True):
+ """Returns Y data coordinates.
+
+ :param bool copy: True to get a copy,
+ False to return internal array (do not modify!)
+ :return: Y coordinates
+ :rtype: numpy.ndarray
+ """
+ return numpy.array(self._y, copy=copy)
+
+ def getValues(self, copy=True):
+ """Returns data values.
+
+ :param bool copy: True to get a copy,
+ False to return internal array (do not modify!)
+ :return: data values
+ :rtype: numpy.ndarray
+ """
+ return numpy.array(self._value, copy=copy)
+
+ def _updateScene(self):
+ self._getScenePrimitive().children = [] # Remove previous primitives
+
+ if not self.isVisible():
+ return # Update when visible
+
+ x, y, value = self.getData(copy=False)
+ if len(x) == 0:
+ return # Nothing to display
+
+ mode = self.getVisualization()
+ heightMap = self.isHeightMap()
+
+ if mode == 'points':
+ z = value if heightMap else 0.
+ symbol, size = self._getSceneSymbol()
+ primitive = primitives.Points(
+ 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:
+ coordinates = numpy.array((x, y)).T
+
+ if len(coordinates) > 3:
+ # Enough points to try a Delaunay tesselation
+
+ # Lazy loading of Delaunay
+ from silx.third_party.scipy_spatial import Delaunay as _Delaunay
+
+ try:
+ tri = _Delaunay(coordinates)
+ except RuntimeError:
+ _logger.error("Delaunay tesselation failed: %s",
+ sys.exc_info()[1])
+ return None
+
+ self._cachedTrianglesIndices = numpy.ravel(
+ tri.simplices.astype(numpy.uint32))
+
+ else:
+ # 3 or less points: Draw one triangle
+ self._cachedTrianglesIndices = \
+ numpy.arange(3, dtype=numpy.uint32) % len(coordinates)
+
+ if mode == 'lines' and self._cachedLinesIndices is None:
+ # Compute line indices
+ self._cachedLinesIndices = utils.triangleToLineIndices(
+ self._cachedTrianglesIndices, unicity=True)
+
+ if mode == 'lines':
+ indices = self._cachedLinesIndices
+ renderMode = 'lines'
+ else:
+ indices = self._cachedTrianglesIndices
+ renderMode = 'triangles'
+
+ # TODO supports x, y instead of copy
+ if heightMap:
+ if len(value) == 1:
+ value = numpy.ones_like(x) * value
+ coordinates = numpy.array((x, y, value), dtype=numpy.float32).T
+ else:
+ coordinates = numpy.array((x, y), dtype=numpy.float32).T
+
+ # TODO option to enable/disable light, cache normals
+ # TODO smooth surface
+ if mode == 'solid':
+ if heightMap:
+ coordinates = coordinates[indices]
+ if len(value) > 1:
+ value = value[indices]
+ triangleNormals = utils.trianglesNormal(coordinates)
+ 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.)
+ else:
+ normal = None
+
+ primitive = primitives.ColormapMesh3D(
+ coordinates,
+ value.reshape(-1, 1), # Makes it a 2D array
+ normal=normal,
+ colormap=self._getSceneColormap(),
+ indices=indices,
+ mode=renderMode)
+ primitive.lineWidth = self.getLineWidth()
+ primitive.lineSmooth = False
+
+ self._getScenePrimitive().children = [primitive]