diff options
Diffstat (limited to 'silx/gui/plot3d/items')
-rw-r--r-- | silx/gui/plot3d/items/__init__.py | 4 | ||||
-rw-r--r-- | silx/gui/plot3d/items/mesh.py | 5 | ||||
-rw-r--r-- | silx/gui/plot3d/items/mixins.py | 18 | ||||
-rw-r--r-- | silx/gui/plot3d/items/scatter.py | 108 | ||||
-rw-r--r-- | silx/gui/plot3d/items/volume.py | 308 |
5 files changed, 323 insertions, 120 deletions
diff --git a/silx/gui/plot3d/items/__init__.py b/silx/gui/plot3d/items/__init__.py index 58eee9c..5810618 100644 --- a/silx/gui/plot3d/items/__init__.py +++ b/silx/gui/plot3d/items/__init__.py @@ -34,10 +34,10 @@ __date__ = "15/11/2017" from .core import DataItem3D, Item3D, GroupItem, GroupWithAxesItem # noqa from .core import ItemChangedType, Item3DChangedType # noqa -from .mixins import (ColormapMixIn, InterpolationMixIn, # noqa +from .mixins import (ColormapMixIn, ComplexMixIn, InterpolationMixIn, # noqa PlaneMixIn, SymbolMixIn) # noqa from .clipplane import ClipPlane # noqa from .image import ImageData, ImageRgba # noqa from .mesh import Mesh, ColormapMesh, Box, Cylinder, Hexagon # noqa from .scatter import Scatter2D, Scatter3D # noqa -from .volume import ScalarField3D # noqa +from .volume import ComplexField3D, ScalarField3D # noqa diff --git a/silx/gui/plot3d/items/mesh.py b/silx/gui/plot3d/items/mesh.py index d3f5e38..3577dbf 100644 --- a/silx/gui/plot3d/items/mesh.py +++ b/silx/gui/plot3d/items/mesh.py @@ -35,6 +35,7 @@ __date__ = "17/07/2018" import logging import numpy +from ... import _glutils as glu from ..scene import primitives, utils, function from ..scene.transform import Rotate from .core import DataItem3D, ItemChangedType @@ -168,7 +169,7 @@ class _MeshBase(DataItem3D): _logger.warning("Unsupported draw mode: %s" % mode) return None - trianglesIndices, t, barycentric = utils.segmentTrianglesIntersection( + trianglesIndices, t, barycentric = glu.segmentTrianglesIntersection( rayObject, triangles) if len(trianglesIndices) == 0: @@ -494,7 +495,7 @@ class _CylindricalVolume(DataItem3D): positions = self._mesh.getAttribute('position', copy=False) triangles = positions.reshape(-1, 3, 3) # 'triangle' draw mode - trianglesIndices, t = utils.segmentTrianglesIntersection( + trianglesIndices, t = glu.segmentTrianglesIntersection( rayObject, triangles)[:2] if len(trianglesIndices) == 0: diff --git a/silx/gui/plot3d/items/mixins.py b/silx/gui/plot3d/items/mixins.py index 40b8438..b355627 100644 --- a/silx/gui/plot3d/items/mixins.py +++ b/silx/gui/plot3d/items/mixins.py @@ -38,6 +38,7 @@ from silx.math.combo import min_max from ...plot.items.core import ItemMixInBase from ...plot.items.core import ColormapMixIn as _ColormapMixIn from ...plot.items.core import SymbolMixIn as _SymbolMixIn +from ...plot.items.core import ComplexMixIn as _ComplexMixIn from ...colors import rgba from ..scene import primitives @@ -139,8 +140,9 @@ class ColormapMixIn(_ColormapMixIn): self._dataRange = dataRange - if self.getColormap().isAutoscale(): - self._syncSceneColormap() + colormap = self.getColormap() + if None in (colormap.getVMin(), colormap.getVMax()): + self._colormapChanged() def _getDataRange(self): """Returns the data range as used in the scene for colormap @@ -173,6 +175,18 @@ class ColormapMixIn(_ColormapMixIn): self.__sceneColormap.range_ = range_ +class ComplexMixIn(_ComplexMixIn): + __doc__ = _ComplexMixIn.__doc__ # Reuse docstring + + _SUPPORTED_COMPLEX_MODES = ( + _ComplexMixIn.ComplexMode.REAL, + _ComplexMixIn.ComplexMode.IMAGINARY, + _ComplexMixIn.ComplexMode.ABSOLUTE, + _ComplexMixIn.ComplexMode.PHASE, + _ComplexMixIn.ComplexMode.SQUARE_AMPLITUDE) + """Overrides supported ComplexMode""" + + class SymbolMixIn(_SymbolMixIn): """Mix-in class for symbol and symbolSize properties for Item3D""" diff --git a/silx/gui/plot3d/items/scatter.py b/silx/gui/plot3d/items/scatter.py index b7bcd09..e8ffee1 100644 --- a/silx/gui/plot3d/items/scatter.py +++ b/silx/gui/plot3d/items/scatter.py @@ -31,14 +31,19 @@ __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "15/11/2017" -import collections +try: + from collections import abc +except ImportError: # Python2 support + import collections as abc import logging -import sys import numpy 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 from .core import DataItem3D, Item3DChangedType, ItemChangedType from .mixins import ColormapMixIn, SymbolMixIn from ._pick import PickingResult @@ -213,16 +218,19 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn): return None -class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn): +class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn, + ScatterVisualizationMixIn): """2D scatter data with settable visualization mode. :param parent: The View widget this item belongs to. """ _VISUALIZATION_PROPERTIES = { - 'points': ('symbol', 'symbolSize'), - 'lines': ('lineWidth',), - 'solid': (), + ScatterVisualizationMixIn.Visualization.POINTS: + ('symbol', 'symbolSize'), + ScatterVisualizationMixIn.Visualization.LINES: + ('lineWidth',), + ScatterVisualizationMixIn.Visualization.SOLID: (), } """Dict {visualization mode: property names used in this mode}""" @@ -230,8 +238,8 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn): DataItem3D.__init__(self, parent=parent) ColormapMixIn.__init__(self) SymbolMixIn.__init__(self) + ScatterVisualizationMixIn.__init__(self) - self._visualizationMode = 'points' self._heightMap = False self._lineWidth = 1. @@ -254,48 +262,14 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn): child.marker = symbol child.setAttribute('size', size, copy=True) - elif event == ItemChangedType.VISIBLE: + elif event is 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 tessellation-based wireframe representation - - 'solid': For Delaunay tessellation-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 + elif event is ItemChangedType.VISUALIZATION_MODE: self._updateScene() - self._updated(ItemChangedType.VISUALIZATION_MODE) - def getVisualization(self): - """Returns the current visualization mode. - - See :meth:`setVisualization` - - :rtype: str - """ - return self._visualizationMode + super(Scatter2D, self)._updated(event) def isPropertyEnabled(self, name, visualization=None): """Returns true if the property is used with visualization mode. @@ -374,7 +348,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn): y, copy=copy, dtype=numpy.float32, order='C').reshape(-1) assert len(x) == len(y) - if isinstance(value, collections.Iterable): + if isinstance(value, abc.Iterable): value = numpy.array( value, copy=copy, dtype=numpy.float32, order='C').reshape(-1) assert len(value) == len(x) @@ -503,7 +477,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn): trianglesIndices = self._cachedTrianglesIndices.reshape(-1, 3) triangles = points[trianglesIndices, :3] - selectedIndices, t, barycentric = utils.segmentTrianglesIntersection( + selectedIndices, t, barycentric = glu.segmentTrianglesIntersection( rayObject, triangles) closest = numpy.argmax(barycentric, axis=1) @@ -542,14 +516,14 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn): numpy.ones_like(xData))) mode = self.getVisualization() - if mode == 'points': + 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)) - elif mode == 'lines': + elif mode is self.Visualization.LINES: # Picking only at point return self._pickPoints(context, points, threshold=5.) @@ -569,7 +543,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn): mode = self.getVisualization() heightMap = self.isHeightMap() - if mode == 'points': + if mode is self.Visualization.POINTS: z = value if heightMap else 0. symbol, size = self._getSceneSymbol() primitive = primitives.Points( @@ -582,35 +556,19 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn): # 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: + triangulation = delaunay(x, y) + if triangulation is None: + return None + self._cachedTrianglesIndices = numpy.ravel( + triangulation.simplices.astype(numpy.uint32)) + + if (mode is self.Visualization.LINES and + self._cachedLinesIndices is None): # Compute line indices self._cachedLinesIndices = utils.triangleToLineIndices( self._cachedTrianglesIndices, unicity=True) - if mode == 'lines': + if mode is self.Visualization.LINES: indices = self._cachedLinesIndices renderMode = 'lines' else: @@ -627,7 +585,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn): # TODO option to enable/disable light, cache normals # TODO smooth surface - if mode == 'solid': + if mode is self.Visualization.SOLID: if heightMap: coordinates = coordinates[indices] if len(value) > 1: diff --git a/silx/gui/plot3d/items/volume.py b/silx/gui/plot3d/items/volume.py index 08ad02a..ae91e82 100644 --- a/silx/gui/plot3d/items/volume.py +++ b/silx/gui/plot3d/items/volume.py @@ -38,13 +38,15 @@ import numpy from silx.math.combo import min_max from silx.math.marchingcubes import MarchingCubes +from ....utils.proxy import docstring +from ... import _glutils as glu from ... import qt from ...colors import rgba from ..scene import cutplane, primitives, transform, utils from .core import BaseNodeItem, Item3D, ItemChangedType, Item3DChangedType -from .mixins import ColormapMixIn, InterpolationMixIn, PlaneMixIn +from .mixins import ColormapMixIn, ComplexMixIn, InterpolationMixIn, PlaneMixIn from ._pick import PickingResult @@ -60,12 +62,13 @@ class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn): def __init__(self, parent): plane = cutplane.CutPlane(normal=(0, 1, 0)) - Item3D.__init__(self, parent=parent) + Item3D.__init__(self, parent=None) ColormapMixIn.__init__(self) InterpolationMixIn.__init__(self) PlaneMixIn.__init__(self, plane=plane) self._dataRange = None + self._data = None self._getScenePrimitive().children = [plane] @@ -73,20 +76,53 @@ class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn): ColormapMixIn._setSceneColormap(self, plane.colormap) InterpolationMixIn._setPrimitive(self, plane) - parent.sigItemChanged.connect(self._parentChanged) + self.setParent(parent) + + def _updateData(self, data, range_): + """Update used dataset + + No copy is made. + + :param Union[numpy.ndarray[float],None] data: The dataset + :param Union[List[float],None] range_: + (min, min positive, max) values + """ + self._data = None if data is None else numpy.array(data, copy=False) + self._getPlane().setData(self._data, copy=False) + + # Store data range info as 3-tuple of values + self._dataRange = range_ + self._setRangeFromData( + None if self._dataRange is None else numpy.array(self._dataRange)) + + self._updated(ItemChangedType.DATA) + + def _syncDataWithParent(self): + """Synchronize this instance data with that of its parent""" + parent = self.parent() + if parent is None: + data, range_ = None, None + else: + data = parent.getData(copy=False) + range_ = parent.getDataRange() + self._updateData(data, range_) def _parentChanged(self, event): """Handle data change in the parent this plane belongs to""" if event == ItemChangedType.DATA: - data = self.sender().getData(copy=False) - self._getPlane().setData(data, copy=False) + self._syncDataWithParent() + + def setParent(self, parent): + oldParent = self.parent() + if isinstance(oldParent, Item3D): + oldParent.sigItemChanged.disconnect(self._parentChanged) - # 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)) + super(CutPlane, self).setParent(parent) - self._updated(ItemChangedType.DATA) + if isinstance(parent, Item3D): + parent.sigItemChanged.connect(self._parentChanged) + + self._syncDataWithParent() # Colormap @@ -114,8 +150,9 @@ class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn): positive min is NaN if no data is positive. :return: (min, positive min, max) or None. + :rtype: Union[List[float],None] """ - return self._dataRange + return None if self._dataRange is None else tuple(self._dataRange) def getData(self, copy=True): """Return 3D dataset. @@ -125,8 +162,10 @@ class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn): False to get the internal data (DO NOT modify!) :return: The data set (or None if not set) """ - parent = self.parent() - return None if parent is None else parent.getData(copy=copy) + if self._data is None: + return None + else: + return numpy.array(self._data, copy=copy) def _pickFull(self, context): """Perform picking in this item at given widget position. @@ -172,18 +211,38 @@ class Isosurface(Item3D): """ def __init__(self, parent): - Item3D.__init__(self, parent=parent) - assert isinstance(parent, ScalarField3D) - parent.sigItemChanged.connect(self._scalarField3DChanged) + Item3D.__init__(self, parent=None) + self._data = None self._level = float('nan') self._autoLevelFunction = None self._color = rgba('#FFD700FF') + self.setParent(parent) + + def _syncDataWithParent(self): + """Synchronize this instance data with that of its parent""" + parent = self.parent() + if parent is None: + self._data = None + else: + self._data = parent.getData(copy=False) self._updateScenePrimitive() - def _scalarField3DChanged(self, event): - """Handle parent's ScalarField3D sigItemChanged""" + def _parentChanged(self, event): + """Handle data change in the parent this isosurface belongs to""" if event == ItemChangedType.DATA: - self._updateScenePrimitive() + self._syncDataWithParent() + + def setParent(self, parent): + oldParent = self.parent() + if isinstance(oldParent, Item3D): + oldParent.sigItemChanged.disconnect(self._parentChanged) + + super(Isosurface, self).setParent(parent) + + if isinstance(parent, Item3D): + parent.sigItemChanged.connect(self._parentChanged) + + self._syncDataWithParent() def getData(self, copy=True): """Return 3D dataset. @@ -193,8 +252,10 @@ class Isosurface(Item3D): False to get the internal data (DO NOT modify!) :return: The data set (or None if not set) """ - parent = self.parent() - return None if parent is None else parent.getData(copy=copy) + if self._data is None: + return None + else: + return numpy.array(self._data, copy=copy) def getLevel(self): """Return the level of this iso-surface (float)""" @@ -349,7 +410,7 @@ class Isosurface(Item3D): mc = MarchingCubes(data.reshape(2, 2, 2), isolevel=level) points = mc.get_vertices() + currentBin triangles = points[mc.get_indices()] - t = utils.segmentTrianglesIntersection(rayObject, triangles)[1] + t = glu.segmentTrianglesIntersection(rayObject, triangles)[1] t = numpy.unique(t) # Duplicates happen on triangle edges if len(t) != 0: # Compute intersection points and get closest data point @@ -372,6 +433,12 @@ class ScalarField3D(BaseNodeItem): :param parent: The View widget this item belongs to. """ + _CutPlane = CutPlane + """CutPlane class associated to this class""" + + _Isosurface = Isosurface + """Isosurface classe associated to this class""" + def __init__(self, parent=None): BaseNodeItem.__init__(self, parent=parent) @@ -385,7 +452,7 @@ class ScalarField3D(BaseNodeItem): self._data = None self._dataRange = None - self._cutPlane = CutPlane(parent=self) + self._cutPlane = self._CutPlane(parent=self) self._cutPlane.setVisible(False) self._isogroup = primitives.GroupDepthOffset() @@ -405,6 +472,26 @@ class ScalarField3D(BaseNodeItem): self._cutPlane._getScenePrimitive(), self._isogroup] + @staticmethod + def _computeRangeFromData(data): + """Compute range info (min, min positive, max) from data + + :param Union[numpy.ndarray,None] data: + :return: Union[List[float],None] + """ + if data is None: + return None + + dataRange = min_max(data, min_positive=True, finite=True) + if dataRange.minimum is None: # Only non-finite data + return None + + if dataRange is not None: + min_positive = dataRange.min_positive + if min_positive is None: + min_positive = float('nan') + return dataRange.minimum, min_positive, dataRange.maximum + def setData(self, data, copy=True): """Set the 3D scalar data represented by this item. @@ -418,7 +505,6 @@ class ScalarField3D(BaseNodeItem): """ if data is None: self._data = None - self._dataRange = None self._boundedGroup.shape = None else: @@ -427,21 +513,9 @@ class ScalarField3D(BaseNodeItem): assert min(data.shape) >= 2 self._data = data - - # Store data range info - dataRange = min_max(self._data, min_positive=True, finite=True) - if dataRange.minimum is None: # Only non-finite data - dataRange = None - - if dataRange is not None: - min_positive = dataRange.min_positive - if min_positive is None: - min_positive = float('nan') - dataRange = dataRange.minimum, min_positive, dataRange.maximum - self._dataRange = dataRange - self._boundedGroup.shape = self._data.shape + self._dataRange = self._computeRangeFromData(self._data) self._updated(ItemChangedType.DATA) def getData(self, copy=True): @@ -506,7 +580,7 @@ class ScalarField3D(BaseNodeItem): :return: isosurface object :rtype: ~silx.gui.plot3d.items.volume.Isosurface """ - isosurface = Isosurface(parent=self) + isosurface = self._Isosurface(parent=self) isosurface.setColor(color) if callable(level): isosurface.setAutoLevelFunction(level) @@ -561,8 +635,164 @@ class ScalarField3D(BaseNodeItem): # BaseNodeItem def getItems(self): - """Returns the list of items currently present in the ScalarField3D. + """Returns the list of items currently present in this item. :rtype: tuple """ return self.getCutPlanes() + self.getIsosurfaces() + + +################## +# ComplexField3D # +################## + +class ComplexCutPlane(CutPlane, ComplexMixIn): + """Class representing a cutting plane in a :class:`ComplexField3D` item. + + :param parent: 3D Data set in which the cut plane is applied. + """ + + def __init__(self, parent): + ComplexMixIn.__init__(self) + CutPlane.__init__(self, parent=parent) + + def _syncDataWithParent(self): + """Synchronize this instance data with that of its parent""" + parent = self.parent() + if parent is None: + data, range_ = None, None + else: + mode = self.getComplexMode() + data = parent.getData(mode=mode, copy=False) + range_ = parent.getDataRange(mode=mode) + self._updateData(data, range_) + + def _updated(self, event=None): + """Handle update of the cut plane (and take care of mode change + + :param Union[None,ItemChangedType] event: The kind of update + """ + if event == ItemChangedType.COMPLEX_MODE: + self._syncDataWithParent() + super(ComplexCutPlane, self)._updated(event) + + +class ComplexIsosurface(Isosurface): + """Class representing an iso-surface in a :class:`ComplexField3D` item. + + :param parent: The DataItem3D this iso-surface belongs to + """ + + def __init__(self, parent): + super(ComplexIsosurface, self).__init__(parent) + + def _syncDataWithParent(self): + """Synchronize this instance data with that of its parent""" + parent = self.parent() + if parent is None: + self._data = None + else: + self._data = parent.getData( + mode=parent.getComplexMode(), copy=False) + self._updateScenePrimitive() + + def _parentChanged(self, event): + """Handle data change in the parent this isosurface belongs to""" + if event == ItemChangedType.COMPLEX_MODE: + self._syncDataWithParent() + super(ComplexIsosurface, self)._parentChanged(event) + + +class ComplexField3D(ScalarField3D, ComplexMixIn): + """3D complex field on a regular grid. + + :param parent: The View widget this item belongs to. + """ + + _CutPlane = ComplexCutPlane + _Isosurface = ComplexIsosurface + + def __init__(self, parent=None): + self._dataRangeCache = None + + ComplexMixIn.__init__(self) + ScalarField3D.__init__(self, parent=parent) + + @docstring(ComplexMixIn) + def setComplexMode(self, mode): + if mode != self.getComplexMode(): + self.clearIsosurfaces() # Reset isosurfaces + ComplexMixIn.setComplexMode(self, mode) + + def setData(self, data, copy=True): + """Set the 3D complex data represented by this item. + + Dataset order is zyx (i.e., first dimension is z). + + :param data: 3D array + :type data: 3D numpy.ndarray of float32 with shape at least (2, 2, 2) + :param bool copy: + True (default) to make a copy, + False to avoid copy (DO NOT MODIFY data afterwards) + """ + if data is None: + self._data = None + self._dataRangeCache = None + self._boundedGroup.shape = None + + else: + data = numpy.array(data, copy=copy, dtype=numpy.complex64, order='C') + assert data.ndim == 3 + assert min(data.shape) >= 2 + + self._data = data + self._dataRangeCache = {} + self._boundedGroup.shape = self._data.shape + + self._updated(ItemChangedType.DATA) + + def getData(self, copy=True, mode=None): + """Return 3D dataset. + + This method does not cache data converted to a specific mode, + it computes it for each request. + + :param bool copy: + True (default) to get a copy, + False to get the internal data (DO NOT modify!) + :param Union[None,Mode] mode: + The kind of data to retrieve. + If None (the default), it returns the complex data, + else it computes the requested scalar data. + :return: The data set (or None if not set) + :rtype: Union[numpy.ndarray,None] + """ + if mode is None: + return super(ComplexField3D, self).getData(copy=copy) + else: + return self._convertComplexData(self._data, mode) + + def getDataRange(self, mode=None): + """Return the range of the requested data as a 3-tuple of values. + + Positive min is NaN if no data is positive. + + :param Union[None,Mode] mode: + The kind of data for which to get the range information. + If None (the default), it returns the data range for the current mode, + else it returns the data range for the requested mode. + :return: (min, positive min, max) or None. + :rtype: Union[None,List[float]] + """ + if self._dataRangeCache is None: + return None + + if mode is None: + mode = self.getComplexMode() + + if mode not in self._dataRangeCache: + # Compute it and store it in cache + data = self.getData(copy=False, mode=mode) + self._dataRangeCache[mode] = self._computeRangeFromData(data) + + return self._dataRangeCache[mode] |