summaryrefslogtreecommitdiff
path: root/silx/gui/data/NumpyAxesSelector.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/data/NumpyAxesSelector.py')
-rw-r--r--silx/gui/data/NumpyAxesSelector.py578
1 files changed, 0 insertions, 578 deletions
diff --git a/silx/gui/data/NumpyAxesSelector.py b/silx/gui/data/NumpyAxesSelector.py
deleted file mode 100644
index e6da0d4..0000000
--- a/silx/gui/data/NumpyAxesSelector.py
+++ /dev/null
@@ -1,578 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-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.
-#
-# ###########################################################################*/
-"""This module defines a widget able to convert a numpy array from n-dimensions
-to a numpy array with less dimensions.
-"""
-from __future__ import division
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "29/01/2018"
-
-import logging
-import numpy
-import functools
-from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser
-from silx.gui import qt
-from silx.gui.utils import blockSignals
-import silx.utils.weakref
-
-
-_logger = logging.getLogger(__name__)
-
-
-class _Axis(qt.QWidget):
- """Widget displaying an axis.
-
- It allows to display and scroll in the axis, and provide a widget to
- map the axis with a named axis (the one from the view).
- """
-
- valueChanged = qt.Signal(int)
- """Emitted when the location on the axis change."""
-
- axisNameChanged = qt.Signal(object)
- """Emitted when the user change the name of the axis."""
-
- def __init__(self, parent=None):
- """Constructor
-
- :param parent: Parent of the widget
- """
- super(_Axis, self).__init__(parent)
- self.__axisNumber = None
- self.__customAxisNames = set([])
- self.__label = qt.QLabel(self)
- self.__axes = qt.QComboBox(self)
- self.__axes.currentIndexChanged[int].connect(self.__axisMappingChanged)
- self.__slider = HorizontalSliderWithBrowser(self)
- self.__slider.valueChanged[int].connect(self.__sliderValueChanged)
- layout = qt.QHBoxLayout(self)
- layout.setContentsMargins(0, 0, 0, 0)
- layout.addWidget(self.__label)
- layout.addWidget(self.__axes)
- layout.addWidget(self.__slider, 10000)
- layout.addStretch(1)
- self.setLayout(layout)
-
- def slider(self):
- """Returns the slider used to display axes location.
-
- :rtype: HorizontalSliderWithBrowser
- """
- return self.__slider
-
- def setAxis(self, number, position, size):
- """Set axis information.
-
- :param int number: The number of the axis (from the original numpy
- array)
- :param int position: The current position in the axis (for a slicing)
- :param int size: The size of this axis (0..n)
- """
- self.__label.setText("Dimension %s" % number)
- self.__axisNumber = number
- self.__slider.setMaximum(size - 1)
-
- def axisNumber(self):
- """Returns the axis number.
-
- :rtype: int
- """
- return self.__axisNumber
-
- def setAxisName(self, axisName):
- """Set the current used axis name.
-
- If this name is not available an exception is raised. An empty string
- means that no name is selected.
-
- :param str axisName: The new name of the axis
- :raise ValueError: When the name is not available
- """
- if axisName == "" and self.__axes.count() == 0:
- self.__axes.setCurrentIndex(-1)
- self.__updateSliderVisibility()
- return
-
- for index in range(self.__axes.count()):
- name = self.__axes.itemData(index)
- if name == axisName:
- self.__axes.setCurrentIndex(index)
- self.__updateSliderVisibility()
- return
- raise ValueError("Axis name '%s' not found", axisName)
-
- def axisName(self):
- """Returns the selected axis name.
-
- If no name is selected, an empty string is returned.
-
- :rtype: str
- """
- index = self.__axes.currentIndex()
- if index == -1:
- return ""
- return self.__axes.itemData(index)
-
- def setAxisNames(self, axesNames):
- """Set the available list of names for the axis.
-
- :param List[str] axesNames: List of available names
- """
- self.__axes.clear()
- with blockSignals(self.__axes):
- self.__axes.addItem(" ", "")
- for axis in axesNames:
- self.__axes.addItem(axis, axis)
-
- self.__updateSliderVisibility()
-
- def setCustomAxis(self, axesNames):
- """Set the available list of named axis which can be set to a value.
-
- :param List[str] axesNames: List of customable axis names
- """
- self.__customAxisNames = set(axesNames)
- self.__updateSliderVisibility()
-
- def __axisMappingChanged(self, index):
- """Called when the selected name change.
-
- :param int index: Selected index
- """
- self.__updateSliderVisibility()
- name = self.axisName()
- self.axisNameChanged.emit(name)
-
- def __updateSliderVisibility(self):
- """Update the visibility of the slider according to axis names and
- customable axis names."""
- name = self.axisName()
- isVisible = name == "" or name in self.__customAxisNames
- self.__slider.setVisible(isVisible)
-
- def value(self):
- """Returns the currently selected position in the axis.
-
- :rtype: int
- """
- return self.__slider.value()
-
- def setValue(self, value):
- """Set the currently selected position in the axis.
-
- :param int value:
- """
- self.__slider.setValue(value)
-
- def __sliderValueChanged(self, value):
- """Called when the selected position in the axis change.
-
- :param int value: Position of the axis
- """
- self.valueChanged.emit(value)
-
- def setNamedAxisSelectorVisibility(self, visible):
- """Hide or show the named axis combobox.
-
- If both the selector and the slider are hidden, hide the entire widget.
-
- :param visible: boolean
- """
- self.__axes.setVisible(visible)
- name = self.axisName()
- self.setVisible(visible or name == "")
-
-
-class NumpyAxesSelector(qt.QWidget):
- """Widget to select a view from a numpy array.
-
- .. image:: img/NumpyAxesSelector.png
-
- The widget is set with an input data using :meth:`setData`, and a requested
- output dimension using :meth:`setAxisNames`.
-
- Widgets are provided to selected expected input axis, and a slice on the
- non-selected axis.
-
- The final selected array can be reached using the getter
- :meth:`selectedData`, and the event `selectionChanged`.
-
- If the input data is a HDF5 Dataset, the selected output data will be a
- new numpy array.
- """
-
- dataChanged = qt.Signal()
- """Emitted when the input data change"""
-
- selectedAxisChanged = qt.Signal()
- """Emitted when the selected axis change"""
-
- selectionChanged = qt.Signal()
- """Emitted when the selected data change"""
-
- customAxisChanged = qt.Signal(str, int)
- """Emitted when a custom axis change"""
-
- def __init__(self, parent=None):
- """Constructor
-
- :param parent: Parent of the widget
- """
- super(NumpyAxesSelector, self).__init__(parent)
-
- self.__data = None
- self.__selectedData = None
- self.__axis = []
- self.__axisNames = []
- self.__customAxisNames = set([])
- self.__namedAxesVisibility = True
- layout = qt.QVBoxLayout(self)
- layout.setContentsMargins(0, 0, 0, 0)
- layout.setSizeConstraint(qt.QLayout.SetMinAndMaxSize)
- self.setLayout(layout)
-
- def clear(self):
- """Clear the widget."""
- self.setData(None)
-
- def setAxisNames(self, axesNames):
- """Set the axis names of the output selected data.
-
- Axis names are defined from slower to faster axis.
-
- The size of the list will constrain the dimension of the resulting
- array.
-
- :param List[str] axesNames: List of distinct strings identifying axis names
- """
- self.__axisNames = list(axesNames)
- assert len(set(self.__axisNames)) == len(self.__axisNames),\
- "Non-unique axes names: %s" % self.__axisNames
-
- delta = len(self.__axis) - len(self.__axisNames)
- if delta < 0:
- delta = 0
- for index, axis in enumerate(self.__axis):
- with blockSignals(axis):
- axis.setAxisNames(self.__axisNames)
- if index >= delta and index - delta < len(self.__axisNames):
- axis.setAxisName(self.__axisNames[index - delta])
- else:
- axis.setAxisName("")
- self.__updateSelectedData()
-
- def setCustomAxis(self, axesNames):
- """Set the available list of named axis which can be set to a value.
-
- :param List[str] axesNames: List of customable axis names
- """
- self.__customAxisNames = set(axesNames)
- for axis in self.__axis:
- axis.setCustomAxis(self.__customAxisNames)
-
- def setData(self, data):
- """Set the input data unsed by the widget.
-
- :param numpy.ndarray data: The input data
- """
- if self.__data is not None:
- # clean up
- for widget in self.__axis:
- self.layout().removeWidget(widget)
- widget.deleteLater()
- self.__axis = []
-
- self.__data = data
-
- if data is not None:
- # create expected axes
- dimensionNumber = len(data.shape)
- delta = dimensionNumber - len(self.__axisNames)
- for index in range(dimensionNumber):
- axis = _Axis(self)
- axis.setAxis(index, 0, data.shape[index])
- axis.setAxisNames(self.__axisNames)
- axis.setCustomAxis(self.__customAxisNames)
- if index >= delta and index - delta < len(self.__axisNames):
- axis.setAxisName(self.__axisNames[index - delta])
- # this weak method was expected to be able to delete sub widget
- callback = functools.partial(silx.utils.weakref.WeakMethodProxy(self.__axisValueChanged), axis)
- axis.valueChanged.connect(callback)
- # this weak method was expected to be able to delete sub widget
- callback = functools.partial(silx.utils.weakref.WeakMethodProxy(self.__axisNameChanged), axis)
- axis.axisNameChanged.connect(callback)
- axis.setNamedAxisSelectorVisibility(self.__namedAxesVisibility)
- self.layout().addWidget(axis)
- self.__axis.append(axis)
- self.__normalizeAxisGeometry()
-
- self.dataChanged.emit()
- self.__updateSelectedData()
-
- def __normalizeAxisGeometry(self):
- """Update axes geometry to align all axes components together."""
- if len(self.__axis) <= 0:
- return
- lineEditWidth = max([a.slider().lineEdit().minimumSize().width() for a in self.__axis])
- limitWidth = max([a.slider().limitWidget().minimumSizeHint().width() for a in self.__axis])
- for a in self.__axis:
- a.slider().lineEdit().setFixedWidth(lineEditWidth)
- a.slider().limitWidget().setFixedWidth(limitWidth)
-
- def __axisValueChanged(self, axis, value):
- name = axis.axisName()
- if name in self.__customAxisNames:
- self.customAxisChanged.emit(name, value)
- else:
- self.__updateSelectedData()
-
- def __axisNameChanged(self, axis, name):
- """Called when an axis name change.
-
- :param _Axis axis: The changed axis
- :param str name: The new name of the axis
- """
- names = [x.axisName() for x in self.__axis]
- missingName = set(self.__axisNames) - set(names) - set("")
- if len(missingName) == 0:
- missingName = None
- elif len(missingName) == 1:
- missingName = list(missingName)[0]
- else:
- raise Exception("Unexpected state")
-
- axisChanged = True
-
- if axis.axisName() == "":
- # set the removed label to another widget if it is possible
- availableWidget = None
- for widget in self.__axis:
- if widget is axis:
- continue
- if widget.axisName() == "":
- availableWidget = widget
- break
- if availableWidget is None:
- # If there is no other solution we set the name at the same place
- axisChanged = False
- availableWidget = axis
- with blockSignals(availableWidget):
- availableWidget.setAxisName(missingName)
- else:
- # there is a duplicated name somewhere
- # we swap it with the missing name or with nothing
- dupWidget = None
- for widget in self.__axis:
- if widget is axis:
- continue
- if widget.axisName() == axis.axisName():
- dupWidget = widget
- break
- if missingName is None:
- missingName = ""
- with blockSignals(dupWidget):
- dupWidget.setAxisName(missingName)
-
- if self.__data is None:
- return
- if axisChanged:
- self.selectedAxisChanged.emit()
- self.__updateSelectedData()
-
- def __updateSelectedData(self):
- """Update the selected data according to the state of the widget.
-
- It fires a `selectionChanged` event.
- """
- permutation = self.permutation()
-
- if self.__data is None or permutation is None:
- # No data or not all the expected axes are there
- if self.__selectedData is not None:
- self.__selectedData = None
- self.selectionChanged.emit()
- return
-
- # get a view with few fixed dimensions
- # with a h5py dataset, it create a copy
- # TODO we can reuse the same memory in case of a copy
- self.__selectedData = numpy.transpose(self.__data[self.selection()], permutation)
- self.selectionChanged.emit()
-
- def data(self):
- """Returns the input data.
-
- :rtype: Union[numpy.ndarray,None]
- """
- if self.__data is None:
- return None
- else:
- return numpy.array(self.__data, copy=False)
-
- def selectedData(self):
- """Returns the output data.
-
- This is equivalent to::
-
- numpy.transpose(self.data()[self.selection()], self.permutation())
-
- :rtype: Union[numpy.ndarray,None]
- """
- if self.__selectedData is None:
- return None
- else:
- return numpy.array(self.__selectedData, copy=False)
-
- def permutation(self):
- """Returns the axes permutation to convert data subset to selected data.
-
- If permutation cannot be computer, it returns None.
-
- :rtype: Union[List[int],None]
- """
- if self.__data is None:
- return None
- else:
- indices = []
- for name in self.__axisNames:
- index = 0
- for axis in self.__axis:
- if axis.axisName() == name:
- indices.append(index)
- break
- if axis.axisName() != "":
- index += 1
- else:
- _logger.warning("No axis corresponding to: %s", name)
- return None
- return tuple(indices)
-
- def selection(self):
- """Returns the selection tuple used to slice the data.
-
- :rtype: tuple
- """
- if self.__data is None:
- return tuple()
- else:
- return tuple([axis.value() if axis.axisName() == "" else slice(None)
- for axis in self.__axis])
-
- def setSelection(self, selection, permutation=None):
- """Set the selection along each dimension.
-
- tuple returned by :meth:`selection` can be provided as input,
- provided that it is for the same the number of axes and
- the same number of dimensions of the data.
-
- :param List[Union[int,slice,None]] selection:
- The selection tuple with as one element for each dimension of the data.
- If an element is None, then the whole dimension is selected.
- :param Union[List[int],None] permutation:
- The data axes indices to transpose.
- If not given, no permutation is applied
- :raise ValueError:
- When the selection does not match current data shape and number of axes.
- """
- data_shape = self.__data.shape if self.__data is not None else ()
-
- # Check selection
- if len(selection) != len(data_shape):
- raise ValueError(
- "Selection length (%d) and data ndim (%d) mismatch" %
- (len(selection), len(data_shape)))
-
- # Check selection type
- selectedDataNDim = 0
- for element, size in zip(selection, data_shape):
- if isinstance(element, int):
- if not 0 <= element < size:
- raise ValueError(
- "Selected index (%d) outside data dimension range [0-%d]" %
- (element, size))
- elif element is None or element == slice(None):
- selectedDataNDim += 1
- else:
- raise ValueError("Unsupported element in selection: %s" % element)
-
- ndim = len(self.__axisNames)
- if selectedDataNDim != ndim:
- raise ValueError(
- "Selection dimensions (%d) and number of axes (%d) mismatch" %
- (selectedDataNDim, ndim))
-
- # check permutation
- if permutation is None:
- permutation = tuple(range(ndim))
-
- if set(permutation) != set(range(ndim)):
- raise ValueError(
- "Error in provided permutation: "
- "Wrong size, elements out of range or duplicates")
-
- inversePermutation = numpy.argsort(permutation)
-
- axisNameChanged = False
- customValueChanged = []
- with blockSignals(*self.__axis):
- index = 0
- for element, axis in zip(selection, self.__axis):
- if isinstance(element, int):
- name = ""
- else:
- name = self.__axisNames[inversePermutation[index]]
- index += 1
-
- if axis.axisName() != name:
- axis.setAxisName(name)
- axisNameChanged = True
-
- for element, axis in zip(selection, self.__axis):
- value = element if isinstance(element, int) else 0
- if axis.value() != value:
- axis.setValue(value)
-
- name = axis.axisName()
- if name in self.__customAxisNames:
- customValueChanged.append((name, value))
-
- # Send signals that where disabled
- if axisNameChanged:
- self.selectedAxisChanged.emit()
- for name, value in customValueChanged:
- self.customAxisChanged.emit(name, value)
- self.__updateSelectedData()
-
- def setNamedAxesSelectorVisibility(self, visible):
- """Show or hide the combo-boxes allowing to map the plot axes
- to the data dimension.
-
- :param visible: Boolean
- """
- self.__namedAxesVisibility = visible
- for axis in self.__axis:
- axis.setNamedAxisSelectorVisibility(visible)