summaryrefslogtreecommitdiff
path: root/silx/gui/plot3d/items
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot3d/items')
-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
6 files changed, 253 insertions, 108 deletions
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.