summaryrefslogtreecommitdiff
path: root/silx/gui/plot3d
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot3d')
-rw-r--r--silx/gui/plot3d/ParamTreeView.py2
-rw-r--r--silx/gui/plot3d/ScalarFieldView.py21
-rw-r--r--silx/gui/plot3d/SceneWidget.py30
-rw-r--r--silx/gui/plot3d/_model/items.py67
-rw-r--r--silx/gui/plot3d/items/__init__.py4
-rw-r--r--silx/gui/plot3d/items/core.py4
-rw-r--r--silx/gui/plot3d/items/mesh.py281
-rw-r--r--silx/gui/plot3d/items/mixins.py21
-rw-r--r--silx/gui/plot3d/items/scatter.py39
-rw-r--r--silx/gui/plot3d/items/volume.py12
-rw-r--r--silx/gui/plot3d/scene/primitives.py8
-rw-r--r--silx/gui/plot3d/test/__init__.py4
-rw-r--r--silx/gui/plot3d/test/testSceneWidgetPicking.py53
-rw-r--r--silx/gui/plot3d/test/testStatsWidget.py213
14 files changed, 600 insertions, 159 deletions
diff --git a/silx/gui/plot3d/ParamTreeView.py b/silx/gui/plot3d/ParamTreeView.py
index ee0c876..8cf2b90 100644
--- a/silx/gui/plot3d/ParamTreeView.py
+++ b/silx/gui/plot3d/ParamTreeView.py
@@ -43,7 +43,7 @@ __date__ = "05/12/2017"
import numbers
import sys
-from silx.third_party import six
+import six
from .. import qt
from ..widgets.FloatEdit import FloatEdit as _FloatEdit
diff --git a/silx/gui/plot3d/ScalarFieldView.py b/silx/gui/plot3d/ScalarFieldView.py
index e5e680c..50cba05 100644
--- a/silx/gui/plot3d/ScalarFieldView.py
+++ b/silx/gui/plot3d/ScalarFieldView.py
@@ -886,6 +886,8 @@ class ScalarFieldView(Plot3DWindow):
self._bbox = axes.LabelledAxes()
self._bbox.children = [self._group]
+ self._outerScale = transform.Scale(1., 1., 1.)
+ self._bbox.transforms = [self._outerScale]
self.getPlot3DWidget().viewport.scene.children.append(self._bbox)
self._selectionBox = primitives.Box()
@@ -1204,6 +1206,25 @@ class ScalarFieldView(Plot3DWindow):
# Transformations
+ def setOuterScale(self, sx=1., sy=1., sz=1.):
+ """Set the scale to apply to the whole scene including the axes.
+
+ This is useful when axis lengths in data space are really different.
+
+ :param float sx: Scale factor along the X axis
+ :param float sy: Scale factor along the Y axis
+ :param float sz: Scale factor along the Z axis
+ """
+ self._outerScale.setScale(sx, sy, sz)
+ self.centerScene()
+
+ def getOuterScale(self):
+ """Returns the scales provided by :meth:`setOuterScale`.
+
+ :rtype: numpy.ndarray
+ """
+ return self._outerScale.scale
+
def setScale(self, sx=1., sy=1., sz=1.):
"""Set the scale of the 3D scalar field (i.e., size of a voxel).
diff --git a/silx/gui/plot3d/SceneWidget.py b/silx/gui/plot3d/SceneWidget.py
index 4a824d7..e60dcfc 100644
--- a/silx/gui/plot3d/SceneWidget.py
+++ b/silx/gui/plot3d/SceneWidget.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2019 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,10 +30,11 @@ __authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "24/04/2018"
-import numpy
+import enum
import weakref
-from silx.third_party import enum
+import numpy
+
from .. import qt
from ..colors import rgba
@@ -229,6 +230,9 @@ class SceneSelection(qt.QObject):
:raise ValueError: If the item is not the widget's scene
"""
previous = self.getCurrentItem()
+ if item is previous:
+ return # Fast path, nothing to do
+
if previous is not None:
previous.sigItemChanged.disconnect(self.__currentChanged)
@@ -252,15 +256,18 @@ class SceneSelection(qt.QObject):
'Not an Item3D: %s' % str(item))
current = self.getCurrentItem()
- if current is not previous:
- self.sigCurrentChanged.emit(current, previous)
- self.__updateSelectionModel()
+ self.sigCurrentChanged.emit(current, previous)
+ self.__updateSelectionModel()
def __currentChanged(self, event):
"""Handle updates of the selected item"""
if event == items.Item3DChangedType.ROOT_ITEM:
item = self.sender()
- if item.root() != self.getSceneGroup():
+
+ parent = self.parent()
+ assert isinstance(parent, SceneWidget)
+
+ if item.root() != parent.getSceneGroup():
self.setSelectedItem(None)
# Synchronization with QItemSelectionModel
@@ -488,7 +495,8 @@ class SceneWidget(Plot3DWidget):
:param int index: The index at which to place the item.
By default it is appended to the end of the list.
:return: The newly created scalar volume item
- :rtype: items.ScalarField3D
+ :rtype: ~silx.gui.plot3d.items.volume.ScalarField3D
+
"""
volume = items.ScalarField3D()
volume.setData(data, copy=copy)
@@ -508,7 +516,7 @@ class SceneWidget(Plot3DWidget):
:param int index: The index at which to place the item.
By default it is appended to the end of the list.
:return: The newly created 3D scatter item
- :rtype: items.Scatter3D
+ :rtype: ~silx.gui.plot3d.items.scatter.Scatter3D
"""
scatter3d = items.Scatter3D()
scatter3d.setData(x=x, y=y, z=z, value=value, copy=copy)
@@ -528,7 +536,7 @@ class SceneWidget(Plot3DWidget):
:param int index: The index at which to place the item.
By default it is appended to the end of the list.
:return: The newly created 2D scatter item
- :rtype: items.Scatter2D
+ :rtype: ~silx.gui.plot3d.items.scatter.Scatter2D
"""
scatter2d = items.Scatter2D()
scatter2d.setData(x=x, y=y, value=value, copy=copy)
@@ -548,7 +556,7 @@ class SceneWidget(Plot3DWidget):
:param int index: The index at which to place the item.
By default it is appended to the end of the list.
:return: The newly created image item
- :rtype: items.ImageData or items.ImageRgba
+ :rtype: ~silx.gui.plot3d.items.image.ImageData or ~silx.gui.plot3d.items.image.ImageRgba
:raise ValueError: For arrays of unsupported dimensions
"""
data = numpy.array(data, copy=False)
diff --git a/silx/gui/plot3d/_model/items.py b/silx/gui/plot3d/_model/items.py
index b09f29a..7e58d14 100644
--- a/silx/gui/plot3d/_model/items.py
+++ b/silx/gui/plot3d/_model/items.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2019 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
@@ -38,8 +38,7 @@ import logging
import weakref
import numpy
-
-from silx.third_party import six
+import six
from ...utils.image import convertArrayToQImage
from ...colors import preferredColormaps
@@ -202,7 +201,7 @@ class Settings(StaticRow):
super(Settings, self).__init__(('Settings', None), children=children)
-class Item3DRow(StaticRow):
+class Item3DRow(BaseRow):
"""Represents an :class:`Item3D` with checkable visibility
:param Item3D item: The scene item to represent.
@@ -210,9 +209,8 @@ class Item3DRow(StaticRow):
"""
def __init__(self, item, name=None):
- if name is None:
- name = item.getLabel()
- super(Item3DRow, self).__init__((name, None))
+ self.__name = None if name is None else six.text_type(name)
+ super(Item3DRow, self).__init__()
self.setFlags(
self.flags(0) | qt.Qt.ItemIsUserCheckable | qt.Qt.ItemIsSelectable,
@@ -224,7 +222,8 @@ class Item3DRow(StaticRow):
def _itemChanged(self, event):
"""Handle visibility change"""
- if event == items.ItemChangedType.VISIBLE:
+ if event in (items.ItemChangedType.VISIBLE,
+ items.Item3DChangedType.LABEL):
model = self.model()
if model is not None:
index = self.index(column=1)
@@ -235,16 +234,25 @@ class Item3DRow(StaticRow):
return self._item()
def data(self, column, role):
- if column == 0 and role == qt.Qt.CheckStateRole:
- item = self.item()
- if item is not None and item.isVisible():
- return qt.Qt.Checked
- else:
- return qt.Qt.Unchecked
- elif column == 0 and role == qt.Qt.DecorationRole:
- return icons.getQIcon('item-3dim')
- else:
- return super(Item3DRow, self).data(column, role)
+ if column == 0:
+ if role == qt.Qt.CheckStateRole:
+ item = self.item()
+ if item is not None and item.isVisible():
+ return qt.Qt.Checked
+ else:
+ return qt.Qt.Unchecked
+
+ elif role == qt.Qt.DecorationRole:
+ return icons.getQIcon('item-3dim')
+
+ elif role == qt.Qt.DisplayRole:
+ if self.__name is None:
+ item = self.item()
+ return '' if item is None else item.getLabel()
+ else:
+ return self.__name
+
+ return super(Item3DRow, self).data(column, role)
def setData(self, column, value, role):
if column == 0 and role == qt.Qt.CheckStateRole:
@@ -256,6 +264,9 @@ class Item3DRow(StaticRow):
return False
return super(Item3DRow, self).setData(column, value, role)
+ def columnCount(self):
+ return 2
+
class DataItem3DBoundingBoxRow(ProxyRow):
"""Represents :class:`DataItem3D` bounding box visibility
@@ -562,7 +573,6 @@ class _ColormapBaseProxyRow(ProxyRow):
"""Signal used internally to notify colormap (or data) update"""
def __init__(self, item, *args, **kwargs):
- self._dataRange = None
self._item = weakref.ref(item)
self._colormap = item.getColormap()
@@ -581,19 +591,11 @@ class _ColormapBaseProxyRow(ProxyRow):
:return: Colormap range (min, max)
"""
- if self._dataRange is None:
- item = self.item()
- if item is not None and self._colormap is not None:
- if hasattr(item, 'getDataRange'):
- data = item.getDataRange()
- else:
- data = item.getData(copy=False)
-
- self._dataRange = self._colormap.getColormapRange(data)
-
- else: # Fallback
- self._dataRange = 1, 100
- return self._dataRange
+ item = self.item()
+ if item is not None and self._colormap is not None:
+ return self._colormap.getColormapRange(item._getDataRange())
+ else:
+ return 1, 100 # Fallback
def _modelUpdated(self, *args, **kwargs):
"""Emit dataChanged in the model"""
@@ -624,7 +626,6 @@ class _ColormapBaseProxyRow(ProxyRow):
self._colormap = None
elif event == items.ItemChangedType.DATA:
- self._dataRange = None
self._sigColormapChanged.emit()
diff --git a/silx/gui/plot3d/items/__init__.py b/silx/gui/plot3d/items/__init__.py
index b2a9dab..58eee9c 100644
--- a/silx/gui/plot3d/items/__init__.py
+++ b/silx/gui/plot3d/items/__init__.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2019 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
@@ -38,6 +38,6 @@ from .mixins import (ColormapMixIn, InterpolationMixIn, # noqa
PlaneMixIn, SymbolMixIn) # noqa
from .clipplane import ClipPlane # noqa
from .image import ImageData, ImageRgba # noqa
-from .mesh import Mesh, Box, Cylinder, Hexagon # noqa
+from .mesh import Mesh, ColormapMesh, Box, Cylinder, Hexagon # noqa
from .scatter import Scatter2D, Scatter3D # noqa
from .volume import ScalarField3D # noqa
diff --git a/silx/gui/plot3d/items/core.py b/silx/gui/plot3d/items/core.py
index 0aefced..1745b2b 100644
--- a/silx/gui/plot3d/items/core.py
+++ b/silx/gui/plot3d/items/core.py
@@ -32,10 +32,10 @@ __license__ = "MIT"
__date__ = "15/11/2017"
from collections import defaultdict
+import enum
import numpy
-
-from silx.third_party import enum, six
+import six
from ... import qt
from ...plot.items import ItemChangedType
diff --git a/silx/gui/plot3d/items/mesh.py b/silx/gui/plot3d/items/mesh.py
index 21936ea..d3f5e38 100644
--- a/silx/gui/plot3d/items/mesh.py
+++ b/silx/gui/plot3d/items/mesh.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2019 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
@@ -35,17 +35,18 @@ __date__ = "17/07/2018"
import logging
import numpy
-from ..scene import primitives, utils
+from ..scene import primitives, utils, function
from ..scene.transform import Rotate
from .core import DataItem3D, ItemChangedType
+from .mixins import ColormapMixIn
from ._pick import PickingResult
_logger = logging.getLogger(__name__)
-class Mesh(DataItem3D):
- """Description of mesh.
+class _MeshBase(DataItem3D):
+ """Base class for :class:`Mesh' and :class:`ColormapMesh`.
:param parent: The View widget this item belongs to.
"""
@@ -54,48 +55,22 @@ class Mesh(DataItem3D):
DataItem3D.__init__(self, parent=parent)
self._mesh = None
- def setData(self,
- position,
- color,
- normal=None,
- mode='triangles',
- copy=True):
- """Set mesh geometry data.
-
- Supported drawing modes are: 'triangles', 'triangle_strip', 'fan'
+ def _setMesh(self, mesh):
+ """Set mesh primitive
- :param numpy.ndarray position:
- Position (x, y, z) of each vertex as a (N, 3) array
- :param numpy.ndarray color: Colors for each point or a single color
- :param numpy.ndarray normal: Normals for each point or None (default)
- :param str mode: The drawing mode.
- :param bool copy: True (default) to copy the data,
- False to use as is (do not modify!).
+ :param Union[None,Geometry] mesh: The scene primitive
"""
self._getScenePrimitive().children = [] # Remove any previous mesh
- if position is None or len(position) == 0:
- self._mesh = None
- else:
- self._mesh = primitives.Mesh3D(
- position, color, normal, mode=mode, copy=copy)
+ self._mesh = mesh
+ if self._mesh is not None:
self._getScenePrimitive().children.append(self._mesh)
- self.sigItemChanged.emit(ItemChangedType.DATA)
+ self._updated(ItemChangedType.DATA)
- def getData(self, copy=True):
- """Get the mesh geometry.
-
- :param bool copy:
- True (default) to get a copy,
- False to get internal representation (do not modify!).
- :return: The positions, colors, normals and mode
- :rtype: tuple of numpy.ndarray
- """
- return (self.getPositionData(copy=copy),
- self.getColorData(copy=copy),
- self.getNormalData(copy=copy),
- self.getDrawMode())
+ def _getMesh(self):
+ """Returns the underlying Mesh scene primitive"""
+ return self._mesh
def getPositionData(self, copy=True):
"""Get the mesh vertex positions.
@@ -106,38 +81,38 @@ class Mesh(DataItem3D):
:return: The (x, y, z) positions as a (N, 3) array
:rtype: numpy.ndarray
"""
- if self._mesh is None:
+ if self._getMesh() is None:
return numpy.empty((0, 3), dtype=numpy.float32)
else:
- return self._mesh.getAttribute('position', copy=copy)
+ return self._getMesh().getAttribute('position', copy=copy)
- def getColorData(self, copy=True):
- """Get the mesh vertex colors.
+ def getNormalData(self, copy=True):
+ """Get the mesh vertex normals.
:param bool copy:
True (default) to get a copy,
False to get internal representation (do not modify!).
- :return: The RGBA colors as a (N, 4) array or a single color
- :rtype: numpy.ndarray
+ :return: The normals as a (N, 3) array, a single normal or None
+ :rtype: Union[numpy.ndarray,None]
"""
- if self._mesh is None:
- return numpy.empty((0, 4), dtype=numpy.float32)
+ if self._getMesh() is None:
+ return None
else:
- return self._mesh.getAttribute('color', copy=copy)
+ return self._getMesh().getAttribute('normal', copy=copy)
- def getNormalData(self, copy=True):
- """Get the mesh vertex normals.
+ def getIndices(self, copy=True):
+ """Get the vertex indices.
:param bool copy:
True (default) to get a copy,
False to get internal representation (do not modify!).
- :return: The normals as a (N, 3) array, a single normal or None
- :rtype: numpy.ndarray or None
+ :return: The vertex indices as an array or None.
+ :rtype: Union[numpy.ndarray,None]
"""
- if self._mesh is None:
+ if self._getMesh() is None:
return None
else:
- return self._mesh.getAttribute('normal', copy=copy)
+ return self._getMesh().getIndices(copy=copy)
def getDrawMode(self):
"""Get mesh rendering mode.
@@ -145,7 +120,7 @@ class Mesh(DataItem3D):
:return: The drawing mode of this primitive
:rtype: str
"""
- return self._mesh.drawMode
+ return self._getMesh().drawMode
def _pickFull(self, context):
"""Perform precise picking in this item at given widget position.
@@ -164,28 +139,34 @@ class Mesh(DataItem3D):
return None
mode = self.getDrawMode()
- if mode == 'triangles':
- triangles = positions.reshape(-1, 3, 3)
-
- elif mode == 'triangle_strip':
- # Expand strip
- triangles = numpy.empty((len(positions) - 2, 3, 3),
- dtype=positions.dtype)
- triangles[:, 0] = positions[:-2]
- triangles[:, 1] = positions[1:-1]
- triangles[:, 2] = positions[2:]
-
- elif mode == 'fan':
- # Expand fan
- triangles = numpy.empty((len(positions) - 2, 3, 3),
- dtype=positions.dtype)
- triangles[:, 0] = positions[0]
- triangles[:, 1] = positions[1:-1]
- triangles[:, 2] = positions[2:]
+ vertexIndices = self.getIndices(copy=False)
+ if vertexIndices is not None: # Expand indices
+ positions = utils.unindexArrays(mode, vertexIndices, positions)[0]
+ triangles = positions.reshape(-1, 3, 3)
else:
- _logger.warning("Unsupported draw mode: %s" % mode)
- return None
+ if mode == 'triangles':
+ triangles = positions.reshape(-1, 3, 3)
+
+ elif mode == 'triangle_strip':
+ # Expand strip
+ triangles = numpy.empty((len(positions) - 2, 3, 3),
+ dtype=positions.dtype)
+ triangles[:, 0] = positions[:-2]
+ triangles[:, 1] = positions[1:-1]
+ triangles[:, 2] = positions[2:]
+
+ elif mode == 'fan':
+ # Expand fan
+ triangles = numpy.empty((len(positions) - 2, 3, 3),
+ dtype=positions.dtype)
+ triangles[:, 0] = positions[0]
+ triangles[:, 1] = positions[1:-1]
+ triangles[:, 2] = positions[2:]
+
+ else:
+ _logger.warning("Unsupported draw mode: %s" % mode)
+ return None
trianglesIndices, t, barycentric = utils.segmentTrianglesIntersection(
rayObject, triangles)
@@ -208,12 +189,160 @@ class Mesh(DataItem3D):
indices = trianglesIndices + closest # For corners 1 and 2
indices[closest == 0] = 0 # For first corner (common)
+ if vertexIndices is not None:
+ # Convert from indices in expanded triangles to input vertices
+ indices = vertexIndices[indices]
+
return PickingResult(self,
positions=points,
indices=indices,
fetchdata=self.getPositionData)
+class Mesh(_MeshBase):
+ """Description of mesh.
+
+ :param parent: The View widget this item belongs to.
+ """
+
+ def __init__(self, parent=None):
+ _MeshBase.__init__(self, parent=parent)
+
+ def setData(self,
+ position,
+ color,
+ normal=None,
+ mode='triangles',
+ indices=None,
+ copy=True):
+ """Set mesh geometry data.
+
+ Supported drawing modes are: 'triangles', 'triangle_strip', 'fan'
+
+ :param numpy.ndarray position:
+ Position (x, y, z) of each vertex as a (N, 3) array
+ :param numpy.ndarray color: Colors for each point or a single color
+ :param Union[numpy.ndarray,None] normal: Normals for each point or None (default)
+ :param str mode: The drawing mode.
+ :param Union[List[int],None] indices:
+ Array of vertex indices or None to use arrays directly.
+ :param bool copy: True (default) to copy the data,
+ False to use as is (do not modify!).
+ """
+ assert mode in ('triangles', 'triangle_strip', 'fan')
+ if position is None or len(position) == 0:
+ mesh = None
+ else:
+ mesh = primitives.Mesh3D(
+ position, color, normal, mode=mode, indices=indices, copy=copy)
+ self._setMesh(mesh)
+
+ def getData(self, copy=True):
+ """Get the mesh geometry.
+
+ :param bool copy:
+ True (default) to get a copy,
+ False to get internal representation (do not modify!).
+ :return: The positions, colors, normals and mode
+ :rtype: tuple of numpy.ndarray
+ """
+ return (self.getPositionData(copy=copy),
+ self.getColorData(copy=copy),
+ self.getNormalData(copy=copy),
+ self.getDrawMode())
+
+ def getColorData(self, copy=True):
+ """Get the mesh vertex colors.
+
+ :param bool copy:
+ True (default) to get a copy,
+ False to get internal representation (do not modify!).
+ :return: The RGBA colors as a (N, 4) array or a single color
+ :rtype: numpy.ndarray
+ """
+ if self._getMesh() is None:
+ return numpy.empty((0, 4), dtype=numpy.float32)
+ else:
+ return self._getMesh().getAttribute('color', copy=copy)
+
+
+class ColormapMesh(_MeshBase, ColormapMixIn):
+ """Description of mesh which color is defined by scalar and a colormap.
+
+ :param parent: The View widget this item belongs to.
+ """
+
+ def __init__(self, parent=None):
+ _MeshBase.__init__(self, parent=parent)
+ ColormapMixIn.__init__(self, function.Colormap())
+
+ def setData(self,
+ position,
+ value,
+ normal=None,
+ mode='triangles',
+ indices=None,
+ copy=True):
+ """Set mesh geometry data.
+
+ Supported drawing modes are: 'triangles', 'triangle_strip', 'fan'
+
+ :param numpy.ndarray position:
+ Position (x, y, z) of each vertex as a (N, 3) array
+ :param numpy.ndarray value: Data value for each vertex.
+ :param Union[numpy.ndarray,None] normal: Normals for each point or None (default)
+ :param str mode: The drawing mode.
+ :param Union[List[int],None] indices:
+ Array of vertex indices or None to use arrays directly.
+ :param bool copy: True (default) to copy the data,
+ False to use as is (do not modify!).
+ """
+ assert mode in ('triangles', 'triangle_strip', 'fan')
+ if position is None or len(position) == 0:
+ mesh = None
+ else:
+ mesh = primitives.ColormapMesh3D(
+ position=position,
+ value=numpy.array(value, copy=False).reshape(-1, 1), # Make it a 2D array
+ colormap=self._getSceneColormap(),
+ normal=normal,
+ mode=mode,
+ indices=indices,
+ copy=copy)
+ self._setMesh(mesh)
+
+ # Store data range info
+ ColormapMixIn._setRangeFromData(self, self.getValueData(copy=False))
+
+ def getData(self, copy=True):
+ """Get the mesh geometry.
+
+ :param bool copy:
+ True (default) to get a copy,
+ False to get internal representation (do not modify!).
+ :return: The positions, values, normals and mode
+ :rtype: tuple of numpy.ndarray
+ """
+ return (self.getPositionData(copy=copy),
+ self.getValueData(copy=copy),
+ self.getNormalData(copy=copy),
+ self.getDrawMode())
+
+ def getValueData(self, copy=True):
+ """Get the mesh vertex values.
+
+ :param bool copy:
+ True (default) to get a copy,
+ False to get internal representation (do not modify!).
+ :return: Array of data values
+ :rtype: numpy.ndarray
+ """
+ if self._getMesh() is None:
+ return numpy.empty((0,), dtype=numpy.float32)
+ else:
+ return self._getMesh().getAttribute('value', copy=copy)
+
+
class _CylindricalVolume(DataItem3D):
"""Class that represents a volume with a rotational symmetry along z
@@ -345,7 +474,7 @@ class _CylindricalVolume(DataItem3D):
vertices, color, normals, mode='triangles', copy=False)
self._getScenePrimitive().children.append(self._mesh)
- self.sigItemChanged.emit(ItemChangedType.DATA)
+ self._updated(ItemChangedType.DATA)
def _pickFull(self, context):
"""Perform precise picking in this item at given widget position.
diff --git a/silx/gui/plot3d/items/mixins.py b/silx/gui/plot3d/items/mixins.py
index 8e96441..40b8438 100644
--- a/silx/gui/plot3d/items/mixins.py
+++ b/silx/gui/plot3d/items/mixins.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2019 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
@@ -114,19 +114,17 @@ class ColormapMixIn(_ColormapMixIn):
self.__sceneColormap = sceneColormap
self._syncSceneColormap()
- self.sigItemChanged.connect(self.__colormapUpdated)
-
- def __colormapUpdated(self, event):
+ def _colormapChanged(self):
"""Handle colormap updates"""
- if event == ItemChangedType.COLORMAP:
- self._syncSceneColormap()
+ self._syncSceneColormap()
+ super(ColormapMixIn, self)._colormapChanged()
def _setRangeFromData(self, data=None):
"""Compute the data range the colormap should use from provided data.
:param data: Data set from which to compute the range or None
"""
- if data is None or len(data) == 0:
+ if data is None or data.size == 0:
dataRange = None
else:
dataRange = min_max(data, min_positive=True, finite=True)
@@ -144,6 +142,13 @@ class ColormapMixIn(_ColormapMixIn):
if self.getColormap().isAutoscale():
self._syncSceneColormap()
+ def _getDataRange(self):
+ """Returns the data range as used in the scene for colormap
+
+ :rtype: Union[List[float],None]
+ """
+ return self._dataRange
+
def _setSceneColormap(self, sceneColormap):
"""Set the scene colormap to sync with Colormap object.
@@ -171,8 +176,6 @@ class ColormapMixIn(_ColormapMixIn):
class SymbolMixIn(_SymbolMixIn):
"""Mix-in class for symbol and symbolSize properties for Item3D"""
- _DEFAULT_SYMBOL = 'o'
- _DEFAULT_SYMBOL_SIZE = 7.0
_SUPPORTED_SYMBOLS = collections.OrderedDict((
('o', 'Circle'),
('d', 'Diamond'),
diff --git a/silx/gui/plot3d/items/scatter.py b/silx/gui/plot3d/items/scatter.py
index a13c3db..b7bcd09 100644
--- a/silx/gui/plot3d/items/scatter.py
+++ b/silx/gui/plot3d/items/scatter.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2019 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
@@ -36,6 +36,7 @@ import logging
import sys
import numpy
+from ....utils.deprecation import deprecated
from ..scene import function, primitives, utils
from .core import DataItem3D, Item3DChangedType, ItemChangedType
@@ -43,7 +44,7 @@ from .mixins import ColormapMixIn, SymbolMixIn
from ._pick import PickingResult
-_logger = logging.getLevelName(__name__)
+_logger = logging.getLogger(__name__)
class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
@@ -94,7 +95,7 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
self._scatter.setAttribute('z', z, copy=copy)
self._scatter.setAttribute('value', value, copy=copy)
- ColormapMixIn._setRangeFromData(self, self.getValues(copy=False))
+ ColormapMixIn._setRangeFromData(self, self.getValueData(copy=False))
self._updated(ItemChangedType.DATA)
def getData(self, copy=True):
@@ -107,7 +108,7 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
return (self.getXData(copy),
self.getYData(copy),
self.getZData(copy),
- self.getValues(copy))
+ self.getValueData(copy))
def getXData(self, copy=True):
"""Returns X data coordinates.
@@ -139,7 +140,7 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
"""
return self._scatter.getAttribute('z', copy=copy).reshape(-1)
- def getValues(self, copy=True):
+ def getValueData(self, copy=True):
"""Returns data values.
:param bool copy: True to get a copy,
@@ -149,6 +150,11 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
"""
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'):
"""Perform picking in this item at given widget position.
@@ -202,7 +208,7 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
return PickingResult(self,
positions=dataPoints[picked, :3],
indices=picked,
- fetchdata=self.getValues)
+ fetchdata=self.getValueData)
else:
return None
@@ -269,8 +275,8 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn):
Supported visualization modes are:
- 'points': For scatter plot representation
- - 'lines': For Delaunay tesselation-based wireframe representation
- - 'solid': For Delaunay tesselation-based solid surface representation
+ - 'lines': For Delaunay tessellation-based wireframe representation
+ - 'solid': For Delaunay tessellation-based solid surface representation
:param str mode: Mode of representation to use
"""
@@ -384,7 +390,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn):
self._cachedTrianglesIndices = None
# Store data range info
- ColormapMixIn._setRangeFromData(self, self.getValues(copy=False))
+ ColormapMixIn._setRangeFromData(self, self.getValueData(copy=False))
self._updateScene()
@@ -399,7 +405,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn):
"""
return (self.getXData(copy=copy),
self.getYData(copy=copy),
- self.getValues(copy=copy))
+ self.getValueData(copy=copy))
def getXData(self, copy=True):
"""Returns X data coordinates.
@@ -421,7 +427,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn):
"""
return numpy.array(self._y, copy=copy)
- def getValues(self, copy=True):
+ def getValueData(self, copy=True):
"""Returns data values.
:param bool copy: True to get a copy,
@@ -431,6 +437,11 @@ 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'):
"""Perform picking while in 'points' visualization mode
@@ -472,7 +483,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn):
return PickingResult(self,
positions=points[picked, :3],
indices=picked,
- fetchdata=self.getValues)
+ fetchdata=self.getValueData)
else:
return None
@@ -507,7 +518,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn):
return PickingResult(self,
positions=positions,
indices=indices,
- fetchdata=self.getValues)
+ fetchdata=self.getValueData)
def _pickFull(self, context):
"""Perform picking in this item at given widget position.
@@ -521,7 +532,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn):
return None
if self.isHeightMap():
- zData = self.getValues(copy=False)
+ zData = self.getValueData(copy=False)
else:
zData = numpy.zeros_like(xData)
diff --git a/silx/gui/plot3d/items/volume.py b/silx/gui/plot3d/items/volume.py
index ca22f1f..08ad02a 100644
--- a/silx/gui/plot3d/items/volume.py
+++ b/silx/gui/plot3d/items/volume.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2019 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
@@ -78,13 +78,15 @@ class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn):
def _parentChanged(self, event):
"""Handle data change in the parent this plane belongs to"""
if event == ItemChangedType.DATA:
- self._getPlane().setData(self.sender().getData(copy=False),
- copy=False)
+ data = self.sender().getData(copy=False)
+ self._getPlane().setData(data, copy=False)
# Store data range info as 3-tuple of values
self._dataRange = self.sender().getDataRange()
+ self._setRangeFromData(
+ None if self._dataRange is None else numpy.array(self._dataRange))
- self.sigItemChanged.emit(ItemChangedType.DATA)
+ self._updated(ItemChangedType.DATA)
# Colormap
@@ -104,7 +106,7 @@ class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn):
display = bool(display)
if display != self.getDisplayValuesBelowMin():
self._getPlane().colormap.displayValuesBelowMin = display
- self.sigItemChanged.emit(ItemChangedType.ALPHA)
+ self._updated(ItemChangedType.ALPHA)
def getDataRange(self):
"""Return the range of the data as a 3-tuple of values.
diff --git a/silx/gui/plot3d/scene/primitives.py b/silx/gui/plot3d/scene/primitives.py
index 474581a..ca06e30 100644
--- a/silx/gui/plot3d/scene/primitives.py
+++ b/silx/gui/plot3d/scene/primitives.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2019 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
@@ -1878,11 +1878,13 @@ class ColormapMesh3D(Geometry):
colormap=None,
normal=None,
mode='triangles',
- indices=None):
+ indices=None,
+ copy=True):
super(ColormapMesh3D, self).__init__(mode, indices,
position=position,
normal=normal,
- value=value)
+ value=value,
+ copy=copy)
self._lineWidth = 1.0
self._lineSmooth = True
diff --git a/silx/gui/plot3d/test/__init__.py b/silx/gui/plot3d/test/__init__.py
index c58f307..8825cf4 100644
--- a/silx/gui/plot3d/test/__init__.py
+++ b/silx/gui/plot3d/test/__init__.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2019 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
@@ -59,6 +59,7 @@ def suite():
from .testGL import suite as testGLSuite
from .testScalarFieldView import suite as testScalarFieldViewSuite
from .testSceneWidgetPicking import suite as testSceneWidgetPickingSuite
+ from .testStatsWidget import suite as testStatsWidgetSuite
testsuite = unittest.TestSuite()
testsuite.addTest(testGLSuite())
@@ -66,4 +67,5 @@ def suite():
testsuite.addTest(testScalarFieldViewSuite())
testsuite.addTest(testSceneWidgetPickingSuite())
testsuite.addTest(toolsTestSuite())
+ testsuite.addTest(testStatsWidgetSuite())
return testsuite
diff --git a/silx/gui/plot3d/test/testSceneWidgetPicking.py b/silx/gui/plot3d/test/testSceneWidgetPicking.py
index d0c6467..649fb47 100644
--- a/silx/gui/plot3d/test/testSceneWidgetPicking.py
+++ b/silx/gui/plot3d/test/testSceneWidgetPicking.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2018 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2019 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
@@ -122,7 +122,7 @@ class TestSceneWidgetPicking(TestCaseQt, ParametricTestCase):
self.assertEqual(nbPos, len(data))
self.assertTrue(numpy.array_equal(
data,
- item.getValues()[picking[0].getIndices()]))
+ item.getValueData()[picking[0].getIndices()]))
# Picking outside data
picking = list(self.widget.pickItems(1, 1))
@@ -217,6 +217,55 @@ class TestSceneWidgetPicking(TestCaseQt, ParametricTestCase):
picking = list(self.widget.pickItems(1, 1))
self.assertEqual(len(picking), 0)
+ def testPickMeshWithIndices(self):
+ """Test picking of Mesh items defined by indices"""
+
+ triangles = items.Mesh()
+ triangles.setData(
+ position=((0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 0)),
+ color=(1, 0, 0, 1),
+ indices=numpy.array( # dummy triangles and square
+ (0, 0, 1, 0, 1, 2, 1, 2, 3), dtype=numpy.uint8),
+ mode='triangles')
+ triangleStrip = items.Mesh()
+ triangleStrip.setData(
+ position=((0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 0)),
+ color=(0, 1, 0, 1),
+ indices=numpy.array( # dummy triangles and square
+ (1, 0, 0, 1, 2, 3), dtype=numpy.uint8),
+ mode='triangle_strip')
+ triangleFan = items.Mesh()
+ triangleFan.setData(
+ position=((0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 0)),
+ color=(0, 0, 1, 1),
+ indices=numpy.array( # dummy triangle, square, dummy
+ (1, 1, 0, 2, 3, 3), dtype=numpy.uint8),
+ mode='fan')
+
+ for item in (triangles, triangleStrip, triangleFan):
+ with self.subTest(mode=item.getDrawMode()):
+ # Add item
+ self.widget.clearItems()
+ self.widget.addItem(item)
+ self.widget.resetZoom('front')
+ self.qapp.processEvents()
+
+ # Picking on data (at widget center)
+ picking = list(self.widget.pickItems(*self._widgetCenter()))
+
+ self.assertEqual(len(picking), 1)
+ self.assertIs(picking[0].getItem(), item)
+ nbPos = len(picking[0].getPositions())
+ data = picking[0].getData()
+ self.assertEqual(nbPos, len(data))
+ self.assertTrue(numpy.array_equal(
+ data,
+ item.getPositionData()[picking[0].getIndices()]))
+
+ # Picking outside data
+ picking = list(self.widget.pickItems(1, 1))
+ self.assertEqual(len(picking), 0)
+
def testPickCylindricalMesh(self):
"""Test picking of Box, Cylinder and Hexagon items"""
diff --git a/silx/gui/plot3d/test/testStatsWidget.py b/silx/gui/plot3d/test/testStatsWidget.py
new file mode 100644
index 0000000..1157aec
--- /dev/null
+++ b/silx/gui/plot3d/test/testStatsWidget.py
@@ -0,0 +1,213 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2019 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.
+# ###########################################################################*/
+"""Test silx.gui.plot.StatsWidget with SceneWidget and ScalarFieldView"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "25/01/2019"
+
+
+import unittest
+
+import numpy
+
+from silx.utils.testutils import ParametricTestCase
+from silx.gui.utils.testutils import TestCaseQt
+from silx.gui import qt
+
+from silx.gui.plot.StatsWidget import BasicStatsWidget
+
+from silx.gui.plot3d.ScalarFieldView import ScalarFieldView
+from silx.gui.plot3d.SceneWidget import SceneWidget, items
+
+
+class TestSceneWidget(TestCaseQt, ParametricTestCase):
+ """Tests StatsWidget combined with SceneWidget"""
+
+ def setUp(self):
+ super(TestSceneWidget, self).setUp()
+ self.sceneWidget = SceneWidget()
+ self.sceneWidget.resize(300, 300)
+ self.sceneWidget.show()
+ self.statsWidget = BasicStatsWidget()
+ self.statsWidget.setPlot(self.sceneWidget)
+ # self.qWaitForWindowExposed(self.sceneWidget)
+
+ def tearDown(self):
+ self.qapp.processEvents()
+ self.sceneWidget.setAttribute(qt.Qt.WA_DeleteOnClose)
+ self.sceneWidget.close()
+ del self.sceneWidget
+ self.statsWidget.setAttribute(qt.Qt.WA_DeleteOnClose)
+ self.statsWidget.close()
+ del self.statsWidget
+ super(TestSceneWidget, self).tearDown()
+
+ def test(self):
+ """Test StatsWidget with SceneWidget"""
+ # Prepare scene
+
+ # Data image
+ image = self.sceneWidget.addImage(numpy.arange(100).reshape(10, 10))
+ image.setLabel('Image')
+ # RGB image
+ imageRGB = self.sceneWidget.addImage(
+ numpy.arange(300, dtype=numpy.uint8).reshape(10, 10, 3))
+ imageRGB.setLabel('RGB Image')
+ # 2D scatter
+ data = numpy.arange(100)
+ scatter2D = self.sceneWidget.add2DScatter(x=data, y=data, value=data)
+ scatter2D.setLabel('2D Scatter')
+ # 3D scatter
+ scatter3D = self.sceneWidget.add3DScatter(x=data, y=data, z=data, value=data)
+ scatter3D.setLabel('3D Scatter')
+ # Add a group
+ group = items.GroupItem()
+ self.sceneWidget.addItem(group)
+ # 3D scalar field
+ data = numpy.arange(64**3).reshape(64, 64, 64)
+ scalarField = items.ScalarField3D()
+ scalarField.setData(data, copy=False)
+ scalarField.setLabel('3D Scalar field')
+ group.addItem(scalarField)
+
+ statsTable = self.statsWidget._getStatsTable()
+
+ # Test selection only
+ self.statsWidget.setDisplayOnlyActiveItem(True)
+ self.assertEqual(statsTable.rowCount(), 0)
+
+ self.sceneWidget.selection().setCurrentItem(group)
+ self.assertEqual(statsTable.rowCount(), 0)
+
+ for item in (image, scatter2D, scatter3D, scalarField):
+ with self.subTest('selection only', item=item.getLabel()):
+ self.sceneWidget.selection().setCurrentItem(item)
+ self.assertEqual(statsTable.rowCount(), 1)
+ self._checkItem(item)
+
+ # Test all data
+ self.statsWidget.setDisplayOnlyActiveItem(False)
+ self.assertEqual(statsTable.rowCount(), 4)
+
+ for item in (image, scatter2D, scatter3D, scalarField):
+ with self.subTest('all items', item=item.getLabel()):
+ self._checkItem(item)
+
+ def _checkItem(self, item):
+ """Check that item is in StatsTable and that stats are OK
+
+ :param silx.gui.plot3d.items.Item3D item:
+ """
+ if isinstance(item, (items.Scatter2D, items.Scatter3D)):
+ data = item.getValueData(copy=False)
+ else:
+ data = item.getData(copy=False)
+
+ statsTable = self.statsWidget._getStatsTable()
+ tableItems = statsTable._itemToTableItems(item)
+ self.assertTrue(len(tableItems) > 0)
+ self.assertEqual(tableItems['legend'].text(), item.getLabel())
+ self.assertEqual(float(tableItems['min'].text()), numpy.min(data))
+ self.assertEqual(float(tableItems['max'].text()), numpy.max(data))
+ # TODO
+
+
+class TestScalarFieldView(TestCaseQt):
+ """Tests StatsWidget combined with ScalarFieldView"""
+
+ def setUp(self):
+ super(TestScalarFieldView, self).setUp()
+ self.scalarFieldView = ScalarFieldView()
+ self.scalarFieldView.resize(300, 300)
+ self.scalarFieldView.show()
+ self.statsWidget = BasicStatsWidget()
+ self.statsWidget.setPlot(self.scalarFieldView)
+ # self.qWaitForWindowExposed(self.sceneWidget)
+
+ def tearDown(self):
+ self.qapp.processEvents()
+ self.scalarFieldView.setAttribute(qt.Qt.WA_DeleteOnClose)
+ self.scalarFieldView.close()
+ del self.scalarFieldView
+ self.statsWidget.setAttribute(qt.Qt.WA_DeleteOnClose)
+ self.statsWidget.close()
+ del self.statsWidget
+ super(TestScalarFieldView, self).tearDown()
+
+ def _getTextFor(self, row, name):
+ """Returns text in table at given row for column name
+
+ :param int row: Row number in the table
+ :param str name: Column id
+ :rtype: Union[str,None]
+ """
+ statsTable = self.statsWidget._getStatsTable()
+
+ for column in range(statsTable.columnCount()):
+ headerItem = statsTable.horizontalHeaderItem(column)
+ if headerItem.data(qt.Qt.UserRole) == name:
+ tableItem = statsTable.item(row, column)
+ return tableItem.text()
+
+ return None
+
+ def test(self):
+ """Test StatsWidget with ScalarFieldView"""
+ data = numpy.arange(64**3, dtype=numpy.float64).reshape(64, 64, 64)
+ self.scalarFieldView.setData(data)
+
+ statsTable = self.statsWidget._getStatsTable()
+
+ # Test selection only
+ self.statsWidget.setDisplayOnlyActiveItem(True)
+ self.assertEqual(statsTable.rowCount(), 1)
+
+ # Test all data
+ self.statsWidget.setDisplayOnlyActiveItem(False)
+ self.assertEqual(statsTable.rowCount(), 1)
+
+ for column in range(statsTable.columnCount()):
+ self.assertEqual(float(self._getTextFor(0, 'min')), numpy.min(data))
+ self.assertEqual(float(self._getTextFor(0, 'max')), numpy.max(data))
+ sum_ = numpy.sum(data)
+ comz = numpy.sum(numpy.arange(data.shape[0]) * numpy.sum(data, axis=(1, 2))) / sum_
+ comy = numpy.sum(numpy.arange(data.shape[1]) * numpy.sum(data, axis=(0, 2))) / sum_
+ comx = numpy.sum(numpy.arange(data.shape[2]) * numpy.sum(data, axis=(0, 1))) / sum_
+ self.assertEqual(self._getTextFor(0, 'COM'), str((comx, comy, comz)))
+
+
+def suite():
+ testsuite = unittest.TestSuite()
+ testsuite.addTest(
+ unittest.defaultTestLoader.loadTestsFromTestCase(
+ TestSceneWidget))
+ testsuite.addTest(
+ unittest.defaultTestLoader.loadTestsFromTestCase(
+ TestScalarFieldView))
+ return testsuite
+
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')