Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 7 additions & 16 deletions src/silx/gui/data/DataViews.py
Original file line number Diff line number Diff line change
Expand Up @@ -1464,9 +1464,9 @@ def normalizeData(self, data):
return data

def createWidget(self, parent):
from silx.gui.data.NXdataWidgets import ArrayVolumePlot
from silx.gui.data._NxVolumePlot import NxVolumePlot

widget = ArrayVolumePlot(parent)
widget = NxVolumePlot(parent)
return widget

def axesNames(self, data, info):
Expand All @@ -1479,22 +1479,13 @@ def clear(self):
def setData(self, data):
data = self.normalizeData(data)
nxd = nxdata.get_default(data, validate=False)
signal_name = nxd.signal_name
z_axis, y_axis, x_axis = nxd.axes[-3:]
z_label, y_label, x_label = nxd.axes_names[-3:]
title = nxd.title or signal_name

widget = self.getWidget()
widget.setData(
nxd.signal,
x_axis=x_axis,
y_axis=y_axis,
z_axis=z_axis,
signal_name=signal_name,
xlabel=x_label,
ylabel=y_label,
zlabel=z_label,
title=title,
widget.setVolumeData(
signal=nxd.signal,
signal_name=nxd.signal_name,
axes=nxd.axes,
axes_names=nxd.axes_names,
)

def _getNXDataPriority(self, nxd: NXdata) -> int:
Expand Down
119 changes: 119 additions & 0 deletions src/silx/gui/data/_NxVolumePlot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import logging
from typing import Sequence
import numpy

from silx.gui import qt
from silx.math.calibration import ArrayCalibration, LinearCalibration, NoCalibration
from .NumpyAxesSelector import NumpyAxesSelector
from ..utils import blockSignals


_logger = logging.getLogger(__name__)


class NxVolumePlot(qt.QWidget):
"""
Widget for plotting a NXdata with a nD signal (n >= 3) as a 3D scalar field.

The signal array can have an arbitrary number of dimensions

Sliders are provided to select indices and axis corresponding of the dimensions of
the signal array, and the plot is updated to load the stack corresponding
to the selection.
"""

def __init__(self, parent=None):
super().__init__(parent)

self.__signal: numpy.ndarray | None = None
self.__axes: list[numpy.ndarray | None] | None = None
self.__axes_names: list[str | None] | None = None
from ._VolumeWindow import VolumeWindow

self._view = VolumeWindow(self)

self._axesSelector = NumpyAxesSelector(self)

layout = qt.QVBoxLayout()
layout.addWidget(self._view)
layout.addWidget(self._axesSelector)

self.setLayout(layout)

def setVolumeData(
self,
signal: numpy.ndarray,
signal_name: str | None = None,
axes: Sequence[numpy.ndarray | None] | None = None,
axes_names: Sequence[str | None] | None = None,
):
self.__signal = signal
if axes:
self.__axes = list(axes)
else:
self.__axes = [None] * signal.ndim
if axes_names:
if len(self.__axes) != len(axes_names):
raise ValueError("Axis names must match the length of axes")
self.__axes_names = list(axes_names)
self._axesSelector.selectionChanged.connect(self._updateVolume)
self._axesSelector.selectedAxisChanged.connect(self._updateVolume)

with blockSignals(self._axesSelector):
self._axesSelector.clear()
self._axesSelector.setAxisNames(["Y", "X", "Z"])

# Labels need to be set before the data
if self.__axes_names:
self._axesSelector.setLabels(self.__axes_names)
self._axesSelector.setData(signal)
self._axesSelector.setVisible(signal.ndim > 3)

self.getVolumeView().setWindowTitle(signal_name)
self._updateVolume()

def _updateVolume(self):
"""Update displayed stack according to the current axes selector
data."""
if self.__signal is None or self.__axes is None:
return

axesIndices = self._axesSelector.getIndicesOfNamedAxes()
xIndex = axesIndices["X"]
x_axis = self.__axes[xIndex]
yIndex = axesIndices["Y"]
y_axis = self.__axes[yIndex]
zIndex = axesIndices["Z"]
z_axis = self.__axes[zIndex]

offset = []
scale = []
for axis in [x_axis, y_axis, z_axis]:
if axis is None:
calibration = NoCalibration()
elif len(axis) == 2:
calibration = LinearCalibration(y_intercept=axis[0], slope=axis[1])
else:
calibration = ArrayCalibration(axis)
if not calibration.is_affine():
_logger.warning("Axis has not linear values, ignored")
offset.append(0.0)
scale.append(1.0)
else:
offset.append(calibration(0))
scale.append(calibration.get_slope())

self._view.setData(
self._axesSelector.selectedData(), offset=offset, scale=scale
)
if self.__axes_names:
self._view.setAxesLabels(
self.__axes_names[xIndex],
self.__axes_names[yIndex],
self.__axes_names[zIndex],
)

def clear(self):
with blockSignals(self._axesSelector):
self._axesSelector.clear()
self._view.clear()