Commit 44d0eac2 authored by Matias Guijarro's avatar Matias Guijarro
Browse files

Merge branch 'use-xaxes-for-1d-data2' into 'master'

Flint: Use xaxes for 1d data widget

Closes #2732, #2693, and #2683

See merge request !3686
parents 37dc3957 6413e12a
Pipeline #46907 failed with stages
in 115 minutes and 13 seconds
......@@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Flint
- Added dedicated widget for acqobj exposing 1D data
- Only 1D data from this acqobj is displayed
- Supports metadata from controllers or acqobj to custom the X-axis
- `xaxis_channel`, `xaxis_array`
- Added data display as index for curve plots and onedim plots
- Group MCA channels per detectors in the curve plot property tree
- Added histogram tool when displaying image in custom plots
- Added a content menu option to center profile ROIs in image/scatter
- Added curve stack as custom plot
......
......@@ -692,9 +692,22 @@ class LiveMcaPlot(Plot1D):
ALIASES = ["mca"]
class LiveOneDimPlot(Plot1D):
WIDGET = None
ALIASES = ["onedim"]
CUSTOM_CLASSES = [Plot1D, Plot2D, ScatterView, ImageView, StackView, CurveStack]
LIVE_CLASSES = [LiveCurvePlot, LiveImagePlot, LiveScatterPlot, LiveMcaPlot]
LIVE_CLASSES = [
LiveCurvePlot,
LiveImagePlot,
LiveScatterPlot,
LiveMcaPlot,
LiveOneDimPlot,
]
# For compatibility
CurvePlot = Plot1D
......
......@@ -520,6 +520,7 @@ class FlintClient:
kind: typing.Optional[str] = None,
image_detector: typing.Optional[str] = None,
mca_detector: typing.Optional[str] = None,
onedim_detector: typing.Optional[str] = None,
):
"""Retrieve a live plot.
......@@ -530,6 +531,7 @@ class FlintClient:
kind: Can be one of "default-curve", "default-scatter"
image_detector: Name of the detector displaying image.
mca_detector: Name of the detector displaying MCA data.
onedim_detector: Name of the detector displaying one dim data.
"""
if kind is not None:
if kind == "default-curve":
......@@ -557,6 +559,11 @@ class FlintClient:
plot_id = self.get_live_plot_detector(mca_detector, plot_type="mca")
return plot_class(plot_id=plot_id, flint=self)
elif onedim_detector is not None:
plot_class = plots.LiveOneDimPlot
plot_id = self.get_live_plot_detector(onedim_detector, plot_type="onedim")
return plot_class(plot_id=plot_id, flint=self)
raise ValueError("No plot requested")
def get_plot(
......
......@@ -179,6 +179,7 @@ class FlintApi:
"scatter": plot_item_model.ScatterPlot,
"image": plot_item_model.ImagePlot,
"mca": plot_item_model.McaPlot,
"onedim": plot_item_model.OneDimDataPlot,
"curve": plot_item_model.CurvePlot,
}
return plot_classes[plot_type]
......
......@@ -296,7 +296,7 @@ def createScatterItem(
def createCurveItem(
plot: plot_model.Plot, channel: scan_model.Channel, yAxis: str
plot: plot_model.Plot, channel: scan_model.Channel, yAxis: str, allowIndexed=False
) -> Tuple[plot_model.Item, bool]:
"""
Create an item to a plot using a channel.
......@@ -317,31 +317,39 @@ def createCurveItem(
item.setYAxis(yAxis)
return item, True
else:
xChannel = cloneChannelRef(plot, item.xChannel())
newItem = plot_item_model.CurveItem(plot)
newItem.setXChannel(xChannel)
newItem = None
if allowIndexed:
if isinstance(item, plot_item_model.XIndexCurveItem):
newItem = plot_item_model.XIndexCurveItem(plot)
if newItem is None:
newItem = plot_item_model.CurveItem(plot)
xChannel = cloneChannelRef(plot, item.xChannel())
newItem.setXChannel(xChannel)
newItem.setYChannel(plot_model.ChannelRef(plot, channel.name()))
newItem.setYAxis(yAxis)
else:
# No other x-axis is specified
# Reach another channel name from the same top master
channelNames = []
for device in scan.devices():
if device.topMaster() is not topMaster:
continue
channelNames.extend([c.name() for c in device.channels()])
channelNames.remove(channel.name())
if len(channelNames) > 0:
# Pick the first one
# FIXME: Maybe we could use scan infos to reach the default channel
channelName = channelNames[0]
if allowIndexed:
newItem = plot_item_model.XIndexCurveItem(plot)
else:
# FIXME: Maybe it's better idea to display it with x-index
channelName = channel.name()
channelNames = []
for device in scan.devices():
if device.topMaster() is not topMaster:
continue
channelNames.extend([c.name() for c in device.channels()])
channelNames.remove(channel.name())
if len(channelNames) > 0:
# Pick the first one
# FIXME: Maybe we could use scan infos to reach the default channel
channelName = channelNames[0]
else:
# FIXME: Maybe it's better idea to display it with x-index
channelName = channel.name()
newItem = plot_item_model.CurveItem(plot)
newItem.setXChannel(plot_model.ChannelRef(plot, channelName))
newItem = plot_item_model.CurveItem(plot)
newItem.setXChannel(plot_model.ChannelRef(plot, channelName))
newItem.setYChannel(plot_model.ChannelRef(plot, channel.name()))
newItem.setYAxis(yAxis)
......@@ -709,3 +717,55 @@ def copyItemConfig(sourceItem: plot_model.Item, destinationItem: plot_model.Item
newItem.setSource(destinationSource)
destinationPlot.addItem(newItem)
sourceToDest[item] = newItem
def updateXAxis(
plotModel: plot_model.Plot,
scan: scan_model.Scan,
topMaster: plot_model.Item,
xChannelName: Optional[str] = None,
xIndex: bool = False,
):
"""Update the x-axis used by a plot
Arguments:
xChannelName: If set, Name of the channel to use as X-axis
xIndex: If true, use Y data index as X-axis
"""
# Reach all plot items from this top master
curves = reachAllCurveItemFromDevice(plotModel, scan, topMaster)
if len(curves) == 0:
# FIXME: it would be good to remove that stuff
if xChannelName is not None:
# Create an item to store the x-value
newItem = plot_item_model.CurveItem(plotModel)
newItem.setXChannel(plot_model.ChannelRef(plotModel, xChannelName))
elif xIndex:
newItem = plot_item_model.XIndexCurveItem(plotModel)
plotModel.addItem(newItem)
else:
# Update the x-channel of all this curves
with plotModel.transaction():
useXIndex = isinstance(curves[0], plot_item_model.XIndexCurveItem)
if xChannelName is not None and not useXIndex:
# Update only
for curve in curves:
xChannel = plot_model.ChannelRef(curve, xChannelName)
curve.setXChannel(xChannel)
elif xIndex:
plotModel.clear()
for curve in curves:
item = plot_item_model.XIndexCurveItem(plotModel)
item.setYChannel(curve.yChannel())
item.setYAxis(curve.yAxis())
plotModel.addItem(item)
else:
plotModel.clear()
for curve in curves:
item = plot_item_model.CurveItem(plotModel)
item.setYChannel(curve.yChannel())
item.setYAxis(curve.yAxis())
xChannel = plot_model.ChannelRef(curve, xChannelName)
item.setXChannel(xChannel)
plotModel.addItem(item)
This diff is collapsed.
......@@ -439,7 +439,12 @@ class ManageMainBehaviours(qt.QObject):
deviceName = None
if issubclass(
compatibleModel, (plot_item_model.ImagePlot, plot_item_model.McaPlot)
compatibleModel,
(
plot_item_model.ImagePlot,
plot_item_model.McaPlot,
plot_item_model.OneDimDataPlot,
),
):
plots = [p for p in plots if p.deviceName() == deviceName]
......@@ -643,12 +648,8 @@ class ManageMainBehaviours(qt.QObject):
title = plotModel.name()
if title is None:
if isinstance(plotModel, plot_item_model.OneDimDataPlot):
title = plotModel.deviceName() + " (1D rois)"
elif isinstance(
plotModel, (plot_item_model.ImagePlot, plot_item_model.McaPlot)
):
title = plotModel.deviceName()
if hasattr(plotModel, "plotTitle"):
title = plotModel.plotTitle()
else:
prefix = str(widgetClass.__name__).replace("PlotWidget", "")
title = self.__getUnusedTitle(prefix, workspace)
......@@ -660,6 +661,8 @@ class ManageMainBehaviours(qt.QObject):
name = name.lower() + "-dock"
widget.setWindowTitle(title)
if hasattr(plotModel, "deviceName"):
widget.setDeviceName(plotModel.deviceName())
widget.setObjectName(name)
self.registerDock(widget)
return widget
......
......@@ -218,6 +218,43 @@ class CurveItem(plot_model.Item, CurveMixIn):
)
class XIndexCurveItem(CurveItem):
"""Define a curve as part of a plot.
X is fixed as an index and Y value is defined by a `ChannelRef`.
"""
def isValid(self):
channel = self.yChannel()
return channel is not None
def xData(self, scan: scan_model.Scan) -> Optional[scan_model.Data]:
yData = self.yData(scan)
if yData is None:
return None
y = yData.array()
if y is None:
return None
array = numpy.arange(len(y))
return scan_model.Data(array=array)
def displayName(self, axisName, scan: scan_model.Scan) -> str:
"""Helper to reach the axis display name"""
if axisName == "x":
return "index"
elif axisName == "y":
return self.yChannel().displayName(scan)
else:
assert False
def __str__(self):
return "<%s y=%s yaxis=%s />" % (
type(self).__name__,
self.yChannel(),
self.yAxis(),
)
class McaPlot(plot_model.Plot):
"""Define a plot which is specific for MCAs."""
......@@ -231,6 +268,9 @@ class McaPlot(plot_model.Plot):
def setDeviceName(self, name: str):
self.__deviceName = name
def plotTitle(self) -> str:
return self.__deviceName
def hasSameTarget(self, other: plot_model.Plot) -> bool:
if type(self) is not type(other):
return False
......@@ -239,8 +279,36 @@ class McaPlot(plot_model.Plot):
return True
class OneDimDataPlot(McaPlot):
"""Hack for now to display Lima 1D ROI inside a right widget"""
class OneDimDataPlot(plot_model.Plot):
"""Define a plot which is specific for one dim data.
It is not the same as `CurvePlot` as the content of the channels is 1D for
each steps of the scan.
"""
def __init__(self, parent=None):
plot_model.Plot.__init__(self, parent=parent)
self.__deviceName: Optional[str] = None
self.__plotTitle = "%s"
def setPlotTitle(self, title):
self.__plotTitle = title
def deviceName(self) -> Optional[str]:
return self.__deviceName
def plotTitle(self) -> str:
return self.__plotTitle
def setDeviceName(self, name: str):
self.__deviceName = name
def hasSameTarget(self, other: plot_model.Plot) -> bool:
if type(self) is not type(other):
return False
if self.__deviceName != other.deviceName():
return False
return True
class McaItem(plot_model.Item):
......@@ -287,6 +355,9 @@ class ImagePlot(plot_model.Plot):
def deviceName(self) -> Optional[str]:
return self.__deviceName
def plotTitle(self) -> str:
return self.__deviceName
def setDeviceName(self, name: str):
self.__deviceName = name
......
......@@ -166,6 +166,14 @@ class Plot(qt.QObject):
items.append(i)
return items
def clear(self):
with self.transaction():
for i in reversed(self.__items):
i._setPlot(None)
self.itemRemoved.emit(i)
self.__items = []
self.invalidateStructure()
def removeItem(self, item: Item):
items = self.__itemTree(item)
with self.transaction():
......
......@@ -259,18 +259,33 @@ class Scan(qt.QObject, _Sealable):
raise ValueError("Already in the device list")
self.__devices.append(device)
def getDeviceByName(self, name: str) -> Device:
elements = name.split(":")
for device in self.__devices:
current = device
for e in reversed(elements):
if current is None or current.name() != e:
break
current = current.master()
else:
# The item was found
if current is None:
return device
def getDeviceByName(self, name: str, fromTopMaster=False) -> Device:
"""
Returns a device from an absolute path name.
Arguments:
fromTopMaster: If true, the path is relative to the top master
"""
if fromTopMaster:
for topmaster in self.__devices:
if topmaster.master() is not None:
continue
try:
return self.getDeviceByName(topmaster.name() + ":" + name)
except ValueError:
continue
else:
elements = name.split(":")
for device in self.__devices:
current = device
for e in reversed(elements):
if current is None or current.name() != e:
break
current = current.master()
else:
# The item was found
if current is None:
return device
raise ValueError("Device %s not found." % name)
......@@ -407,13 +422,31 @@ class DeviceType(enum.Enum):
NONE = 0
"""Default type"""
VIRTUAL_ROI = 1
UNKNOWN = -1
"""Unknown value specified in the scan_info"""
LIMA = 1
"""Lima device as specified by the scan_info"""
MCA = 2
"""MCA device as specified by the scan_info"""
VIRTUAL_ROI = 3
"""Device containing channel data from the same ROI.
It is a GUI concept, there is no related device on the BLISS side.
"""
VIRTUAL_MCA_DETECTOR = 4
"""Device containing channel data from a MCA detector.
A MCA device can contain many detectors.
"""
class DeviceMetadata(NamedTuple):
info: Dict
"""raw metadata as stored by the scan_info"""
roi: Optional[object]
"""Define a ROI geometry, is one"""
......@@ -426,7 +459,7 @@ class Device(qt.QObject, _Sealable):
and channels. This could not exactly match the Bliss API.
"""
_noneMetadata = DeviceMetadata(None)
_noneMetadata = DeviceMetadata({}, None)
def __init__(self, parent: Scan):
qt.QObject.__init__(self, parent=parent)
......
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg20"
version="1.1"
viewBox="0 0 32 32"
xml:space="preserve"
sodipodi:docname="item-index.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
inkscape:export-filename="item-index.png"
inkscape:export-xdpi="80"
inkscape:export-ydpi="80"><defs
id="defs1109" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1376"
id="namedview1107"
showgrid="false"
inkscape:zoom="26.185048"
inkscape:cx="13.076808"
inkscape:cy="15.028911"
inkscape:window-x="0"
inkscape:window-y="316"
inkscape:window-maximized="1"
inkscape:current-layer="svg20" /><metadata
id="metadata26"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><path
id="path16"
d="M 29.82965,7.0164302 2.17035,24.98357"
stroke-miterlimit="10"
inkscape:connector-curvature="0"
style="fill:none;stroke:#00a651;stroke-width:1.57369995;stroke-linecap:round;stroke-miterlimit:10"
sodipodi:nodetypes="cc" /></svg>
\ No newline at end of file
......@@ -72,7 +72,9 @@ class ScanRowItem(StandardRowItem):
self.setToolTip(toolTip)
def setChannelLookAndFeel(self, channel: scan_model.Channel):
text = channel.baseName()
text = channel.displayName()
if text is None:
text = channel.baseName()
if channel.type() == scan_model.ChannelType.COUNTER:
icon = icons.getQIcon("flint:icons/channel-curve")
elif channel.type() == scan_model.ChannelType.SPECTRUM:
......
......@@ -435,9 +435,14 @@ class CurvePlotWidget(plot_helper.PlotWidget):
continue
if not item.isVisible():
continue
if isinstance(item, plot_item_model.CurveItem):
if isinstance(item, plot_item_model.XIndexCurveItem):
xLabels.append("index")
elif isinstance(item, plot_item_model.CurveItem):
xAxis = item.xChannel().channel(scan)
xLabels.append(item.xChannel().displayName(scan))
if isinstance(item, plot_item_model.CurveItem):
if item.yAxis() == "left":
y1Labels.append(item.yChannel().displayName(scan))
elif item.yAxis() == "right":
......@@ -747,9 +752,13 @@ class CurvePlotWidget(plot_helper.PlotWidget):
if isinstance(item, plot_item_model.CurveMixIn):
if isinstance(item, plot_item_model.CurveItem):
x = item.xChannel()
if x is None:
xName = "none"
else:
xName = x.name()
y = item.yChannel()
# FIXME: remove legend, use item mapping
legend = x.name() + "/" + y.name() + "/" + str(scan)
legend = f"{xName}/{y.name()}/{str(scan)}"
else:
legend = str(item) + "/" + str(scan)
xx = item.xArray(scan)
......
......@@ -360,6 +360,9 @@ class _AddItemAction(qt.QWidgetAction):
class _DataItem(_property_tree_helper.ScanRowItem):
XAxisIndexRole = 1
def __init__(self):
super(_DataItem, self).__init__()
qt.QStandardItem.__init__(self)
......@@ -370,6 +373,8 @@ class _DataItem(_property_tree_helper.ScanRowItem):
self.__remove = qt.QStandardItem("")
self.__error = qt.QStandardItem("")
self.__xAxisSelected = False
self.__role = None
self.__device = None
self.__plotModel: Optional[plot_model.Plot] = None
self.__plotItem: Optional[plot_model.Item] = None
......@@ -440,7 +445,7 @@ class _DataItem(_property_tree_helper.ScanRowItem):
assert yAxis in ["left", "right"]
_curve, _wasUpdated = model_helper.createCurveItem(
plot, self.__channel, yAxis
plot, self.__channel, yAxis, allowIndexed=True
)
def __visibilityViewChanged(self, item: qt.QStandardItem):
......@@ -464,32 +469,22 @@ class _DataItem(_property_tree_helper.ScanRowItem):
self.__treeView.openPersistentEditor(self.__xaxis.index())
def __xAxisChanged(self, item: qt.QStandardItem):
assert self.__channel is not None
assert self.__plotModel is not None
plotModel = self.__plotModel
# Reach the top master
topMaster = self.__channel.device().topMaster()
scan = topMaster.scan()
# Reach all plot items from this top master
curves = model_helper.reachAllCurveItemFromDevice(
self.__plotModel, scan, topMaster
)
if len(curves) == 0:
# Create an item to store the x-value
plot = self.__plotModel
channelName = self.__channel.name()
newItem = plot_item_model.CurveItem(plot)
newItem.setXChannel(plot_model.ChannelRef(plot, channelName))
plot.addItem(newItem)
if self.__channel is not None:
topMaster = self.__channel.device().topMaster()
scan = topMaster.scan()
xChannelName = self.__channel.name()
model_helper.updateXAxis(
plotModel, scan, topMaster, xChannelName=xChannelName
)
elif self.__role == self.XAxisIndexRole:
topMaster = self.__device.topMaster()