Commit 1beb042d authored by Matias Guijarro's avatar Matias Guijarro
Browse files

Merge branch 'fix-plot-select' into 'master'

Flint: Rework initial curve plot selection (plotselect)

Closes #2065

See merge request bliss/bliss!3723
parents bb2a3b61 b39ef867
Pipeline #47074 failed with stages
in 103 minutes and 36 seconds
......@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Flint
- Added a tool to reset a curve plot to the used plotselect
- 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
......@@ -47,6 +48,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Flint
- When Flint is not fast enough to reach data from Redis, NaN values
are used in order to keep the data alignment
- Improve initial curve plot selection to care about plotselect or user selection
The earlier one have the priority
- XIA mca
- Logger improved (can log at handel lib and BLISS levels)
- Run server according to config retrieved from beacon
......@@ -57,10 +60,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Flint
- Fix initial curve plot selection in order to properly reuse user selection
- Fixed slow rendering occurred on live curves and scatters with fast scans
- The video image is now also used for Lima EXTRERNAL_TRIGGER and EXTERNAL_GATE
- Fixed blinking of the regulation plot legend
- Fixed undisplayed ROIs during a scan. A tool is provided to display them
- Fixed hidden ROIs during a scan. A tool is provided to display them
if not already selected.
- Fixed update of the property view after an update of the backend
- Fixed colormap LUT of the scatter plot when it is set with the style dialog
......
......@@ -139,6 +139,7 @@ from typing import List
import numpy
import functools
import time
from bliss import current_session, is_bliss_shell, global_map
from bliss.common.protocols import Scannable
......@@ -280,6 +281,7 @@ def plotselect(*counters):
scan_display = ScanDisplay()
channel_names = get_channel_names(*counters)
scan_display.displayed_channels = channel_names
scan_display._displayed_channels_time = time.time()
if flint_proxy.check_flint():
flint = flint_proxy.get_flint(mandatory=False)
......
......@@ -648,9 +648,16 @@ def updateDisplayedChannelNames(
def copyItemsFromChannelNames(
sourcePlot: plot_model.Plot, destinationPlot: plot_model.Plot
sourcePlot: plot_model.Plot,
destinationPlot: plot_model.Plot,
scan: scan_model.Scan = None,
):
"""Copy from the source plot the item which was setup into the destination plot"""
"""Copy from the source plot the item which was setup into the destination plot.
If the destination plot do not contain the expected items, the scan is used
to know if they are available, and are then created. Else source item is
skipped.
"""
if not isinstance(sourcePlot, plot_item_model.CurvePlot):
raise TypeError("Only available for curve plot. Found %s" % type(sourcePlot))
if not isinstance(destinationPlot, type(sourcePlot)):
......@@ -674,10 +681,26 @@ def copyItemsFromChannelNames(
if channel is None:
continue
name = channel.name()
sourceItem = availableItems.get(name)
sourceItem = availableItems.pop(name, None)
if sourceItem is not None:
copyItemConfig(sourceItem, item)
if len(availableItems) > 0 and scan is not None:
# Some items could be created
for name, sourceItem in availableItems.items():
channel = scan.getChannelByName(name)
if channel is None:
# Not part of the scan
continue
item, _updated = createCurveItem(
destinationPlot,
channel,
yAxis=sourceItem.yAxis(),
allowIndexed=True,
)
copyItemConfig(sourceItem, item)
def copyItemConfig(sourceItem: plot_model.Item, destinationItem: plot_model.Item):
"""Copy the configuration and the item tree from a source item to a
......
......@@ -586,7 +586,13 @@ def _select_default_counter(scan, plot):
class DisplayExtra(NamedTuple):
displayed_channels: Optional[List[str]]
"""Enforced list of channels to display for this specific scan"""
plotselect: Optional[List[str]]
"""List of name selected by plot select"""
plotselect_time: Optional[int]
"""Time from `time.time()` of the last plotselect"""
def parse_display_extra(scan_info: Dict) -> DisplayExtra:
......@@ -615,10 +621,12 @@ def parse_display_extra(scan_info: Dict) -> DisplayExtra:
)
raw = display_extra.get("plotselect", None)
plotselect = parse_optional_list_of_string(raw, "_display_extra.plotselect")
plotselect_time = display_extra.get("plotselect_time", None)
else:
displayed_channels = None
plotselect = None
return DisplayExtra(displayed_channels, plotselect)
plotselect_time = None
return DisplayExtra(displayed_channels, plotselect, plotselect_time)
def removed_same_plots(plots, remove_plots) -> List[plot_model.Plot]:
......
......@@ -535,20 +535,7 @@ class ManageMainBehaviours(qt.QObject):
self.__updateFocus(defaultWidget, usedWidgets)
def updateWidgetWithPlot(self, widget, scan, plotModel, useDefaultPlot):
previousWidgetPlot = widget.plotModel()
if previousWidgetPlot is not None:
if widget.scan() is None:
previousScanInfo = {}
else:
previousScanInfo = widget.scan().scanInfo()
equivalentPlots = scan_info_helper.is_same(
scan.scanInfo(), previousScanInfo
)
if not equivalentPlots:
previousWidgetPlot = None
# Try to reuse the previous plot
if not useDefaultPlot and previousWidgetPlot is not None:
def reusePreviousPlotItems(previousWidgetPlot, plotModel, scan):
with previousWidgetPlot.transaction():
# Clean up temporary items
for item in list(previousWidgetPlot.items()):
......@@ -562,13 +549,66 @@ class ManageMainBehaviours(qt.QObject):
# FIXME: Make it work first for curves, that's the main use case
if isinstance(previousWidgetPlot, plot_item_model.CurvePlot):
model_helper.copyItemsFromChannelNames(
previousWidgetPlot, plotModel
previousWidgetPlot, plotModel, scan
)
if useDefaultPlot or previousWidgetPlot is None or previousWidgetPlot.isEmpty():
# FIXME: This if-else branches should be the same, but for now
# make it safe for BLISS 1.8
if isinstance(plotModel, plot_item_model.CurvePlot):
# For BLISS 1.8
previousPlotModel = widget.plotModel()
if previousPlotModel is None:
pass
else:
userEditTime = previousPlotModel.userEditTime()
# FIXME: It would be good to hide this parsing
scanPlotselectTime = (
scan.scanInfo()
.get("_display_extra", {})
.get("plotselect_time", None)
)
if userEditTime is not None:
if scanPlotselectTime is None or userEditTime > scanPlotselectTime:
for item in plotModel.items():
model_helper.removeItemAndKeepAxes(plotModel, item)
reusePreviousPlotItems(previousPlotModel, plotModel, scan)
plotModel.setUserEditTime(previousPlotModel.userEditTime())
else:
# Only update the config (dont create new curve items)
reusePreviousPlotItems(previousPlotModel, plotModel, scan=None)
else:
# Only update the config (dont create new curve items)
reusePreviousPlotItems(previousPlotModel, plotModel, scan=None)
if plotModel.styleStrategy() is None:
plotModel.setStyleStrategy(DefaultStyleStrategy(self.__flintModel))
widget.setPlotModel(plotModel)
else:
previousWidgetPlot = widget.plotModel()
if previousWidgetPlot is not None:
if widget.scan() is None:
previousScanInfo = {}
else:
previousScanInfo = widget.scan().scanInfo()
equivalentPlots = scan_info_helper.is_same(
scan.scanInfo(), previousScanInfo
)
if not equivalentPlots:
previousWidgetPlot = None
# Try to reuse the previous plot
if not useDefaultPlot and previousWidgetPlot is not None:
reusePreviousPlotItems(previousWidgetPlot, plotModel, scan=None)
if (
useDefaultPlot
or previousWidgetPlot is None
or previousWidgetPlot.isEmpty()
):
if plotModel.styleStrategy() is None:
plotModel.setStyleStrategy(DefaultStyleStrategy(self.__flintModel))
widget.setPlotModel(plotModel)
previousScan = widget.scan()
self.__clearPreviousScan(previousScan)
......
......@@ -31,6 +31,7 @@ from typing import Any
from typing import Dict
from typing import Optional
import time
import numpy
import enum
import logging
......@@ -101,6 +102,7 @@ class Plot(qt.QObject):
self.__styleStrategy: Optional[StyleStrategy] = None
self.__inTransaction: int = 0
self.__name = None
self.__userEditTime = None
def __reduce__(self):
return (self.__class__, (), self.__getstate__())
......@@ -129,6 +131,18 @@ class Plot(qt.QObject):
"""Returns the name of the plot, if defined."""
return self.__name
def tagUserEditTime(self):
"""Tag the model as edited by the user now"""
self.__userEditTime = time.time()
def setUserEditTime(self, userEditTime: float):
"""Set a specific user edit time"""
self.__userEditTime = userEditTime
def userEditTime(self) -> Optional[float]:
"""Returns the last time the model was edited by the user"""
return self.__userEditTime
def isInTransaction(self) -> bool:
"""True if the plot is in a transaction.
......
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg id="svg6" version="1.1" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<metadata id="metadata2">
<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="path4-8" d="m4.1449 9.8851c1.9122-0.53218 4.28 2.1371 5.5521 0.041934 1.3009-0.93743 2.5392-4.2848 3.8138-3.8579 1.6262 2.0665 3.2525 4.133 4.8787 6.1995 1.347-1.638 2.6939-3.2761 4.0409-4.9141 1.8521 0.73957 3.5266 2.429 5.6325 2.2647" fill="none" stroke="#00a14b" stroke-width="2.1827"/>
<path id="path4-8-6-9" d="m27.88 18.32c-1.6049 0.29995-2.7259-1.1887-4.1732-1.5487-1.4779 0.50128-2.8292 1.3141-4.2328 1.973-0.64569 0.23632-1.0281-0.41623-1.5099-0.70518-0.99097-0.72583-1.9819-1.4517-2.9729-2.1775-1.735 0.9667-3.4701 1.9334-5.2051 2.9001-1.9416-0.23125-3.854-0.78498-5.8242-0.70756" fill="none" stroke="#00a14b" stroke-width="2.1827"/>
<g id="text830" transform="matrix(1.3821 0 0 1.3821 -6.113 -8.9317)" stroke-width=".14018" aria-label="RESET">
<path id="path815" d="m7.8468 24.699q0.33127 0 0.47364-0.1232 0.1451-0.1232 0.1451-0.40519 0-0.27926-0.1451-0.39972-0.14237-0.12046-0.47364-0.12046h-0.44352v1.0486zm-0.44352 0.72826v1.5469h-1.0541v-4.0875h1.6098q0.80765 0 1.1827 0.27104 0.37782 0.27104 0.37782 0.85693 0 0.4052-0.19712 0.66529-0.19438 0.26009-0.58863 0.38329 0.21629 0.04928 0.38603 0.2245 0.17248 0.17248 0.3477 0.52566l0.5722 1.1608h-1.1225l-0.49828-1.0157q-0.15058-0.30663-0.30663-0.41888-0.15332-0.11225-0.41067-0.11225z"/>
<path id="path817" d="m10.664 22.886h2.8446v0.7967h-1.7905v0.76111h1.6837v0.7967h-1.6837v0.93633h1.8508v0.7967h-2.9048z"/>
<path id="path819" d="m17.342 23.015v0.86514q-0.33675-0.15058-0.65707-0.22724t-0.60505-0.07666q-0.37782 0-0.55851 0.10404-0.1807 0.10404-0.1807 0.32306 0 0.16427 0.12046 0.25735 0.1232 0.09035 0.44352 0.15606l0.449 0.09035q0.68171 0.13689 0.96918 0.41615t0.28747 0.79396q0 0.67624-0.40246 1.0075-0.39972 0.32854-1.2238 0.32854-0.38877 0-0.78027-0.07392t-0.78301-0.21902v-0.88979q0.3915 0.20807 0.75563 0.31485 0.36686 0.10404 0.70635 0.10404 0.34496 0 0.5284-0.11499 0.18343-0.11499 0.18343-0.32854 0-0.19165-0.12594-0.29568-0.1232-0.10404-0.49554-0.18617l-0.40793-0.09035q-0.61327-0.13142-0.898-0.41888-0.28199-0.28747-0.28199-0.7748 0-0.61053 0.39424-0.93906t1.1334-0.32854q0.33675 0 0.69266 0.05202 0.35591 0.04928 0.73647 0.15058z"/>
<path id="path821" d="m18.538 22.886h2.8446v0.7967h-1.7905v0.76111h1.6837v0.7967h-1.6837v0.93633h1.8508v0.7967h-2.9048z"/>
<path id="path823" d="m21.884 22.886h3.7672v0.7967h-1.3552v3.2908h-1.0541v-3.2908h-1.358z"/>
</g>
</svg>
......@@ -149,6 +149,9 @@ class YAxesEditor(qt.QWidget):
assert False
if self.__plotItem is not None:
self.__plotItem.setYAxis(axis)
plotModel = self.__plotItem.plot()
# FIXME: It would be better to make it part of the model
plotModel.tagUserEditTime()
self.valueChanged.emit()
def __plotItemChanged(self, eventType):
......@@ -331,11 +334,13 @@ class _AddItemAction(qt.QWidgetAction):
def __createChildItem(self, itemClass):
parentItem = self.parent().selectedPlotItem()
if parentItem is not None:
plot = parentItem.plot()
newItem = itemClass(plot)
plotModel = parentItem.plot()
newItem = itemClass(plotModel)
newItem.setSource(parentItem)
with plot.transaction():
plot.addItem(newItem)
with plotModel.transaction():
plotModel.addItem(newItem)
# FIXME: It would be better to make it part of the model
plotModel.tagUserEditTime()
def __createNormalized(self):
parentItem = self.parent().selectedPlotItem()
......@@ -350,13 +355,15 @@ class _AddItemAction(qt.QWidgetAction):
monitorName = dialog.selectedChannelName()
if monitorName is None:
return
plot = parentItem.plot()
newItem = plot_state_model.NormalizedCurveItem(plot)
channel = plot_model.ChannelRef(plot, monitorName)
plotModel = parentItem.plot()
newItem = plot_state_model.NormalizedCurveItem(plotModel)
channel = plot_model.ChannelRef(plotModel, monitorName)
newItem.setMonitorChannel(channel)
newItem.setSource(parentItem)
with plot.transaction():
plot.addItem(newItem)
with plotModel.transaction():
plotModel.addItem(newItem)
# FIXME: It would be better to make it part of the model
plotModel.tagUserEditTime()
class _DataItem(_property_tree_helper.ScanRowItem):
......@@ -440,18 +447,23 @@ class _DataItem(_property_tree_helper.ScanRowItem):
else:
assert self.__channel is not None
assert self.__plotModel is not None
plot = self.__plotModel
plotModel = self.__plotModel
yAxis = item.data(role=YAxesPropertyItemDelegate.YAxesRole)
assert yAxis in ["left", "right"]
_curve, _wasUpdated = model_helper.createCurveItem(
plot, self.__channel, yAxis, allowIndexed=True
plotModel, self.__channel, yAxis, allowIndexed=True
)
# FIXME: It would be better to make it part of the model
plotModel.tagUserEditTime()
def __visibilityViewChanged(self, item: qt.QStandardItem):
if self.__plotItem is not None:
state = item.data(delegates.VisibilityRole)
self.__plotItem.setVisible(state == qt.Qt.Checked)
plotModel = self.__plotItem.plot()
# FIXME: It would be better to make it part of the model
plotModel.tagUserEditTime()
def setSelectedXAxis(self):
if self.__xAxisSelected:
......@@ -485,6 +497,8 @@ class _DataItem(_property_tree_helper.ScanRowItem):
model_helper.updateXAxis(plotModel, scan, topMaster, xIndex=True)
else:
assert False
# FIXME: It would be better to make it part of the model
plotModel.tagUserEditTime()
def setDevice(self, device: scan_model.Device):
self.setDeviceLookAndFeel(device)
......@@ -676,14 +690,17 @@ class CurvePlotPropertyWidget(qt.QWidget):
def __removeAllItems(self):
if self.__plotModel is None:
return
with self.__plotModel.transaction():
items = list(self.__plotModel.items())
plotModel = self.__plotModel
with plotModel.transaction():
items = list(plotModel.items())
for item in items:
try:
self.__plotModel.removeItem(item)
plotModel.removeItem(item)
except IndexError:
# Item was maybe already removed
pass
# FIXME: It would be better to make it part of the model
plotModel.tagUserEditTime()
def __createToolBar(self):
toolBar = qt.QToolBar(self)
......@@ -691,6 +708,16 @@ class CurvePlotPropertyWidget(qt.QWidget):
action = _AddItemAction(self)
toolBar.addAction(action)
toolBar.addSeparator()
action = qt.QAction(self)
icon = icons.getQIcon("flint:icons/reset-to-plotselect")
action.setIcon(icon)
action.setText("Reset with plotselect")
action.setToolTip("Reset the plot to the original plotselect used")
action.triggered.connect(self.__resetPlotWithOriginalPlot)
toolBar.addAction(action)
action = qt.QAction(self)
icon = icons.getQIcon("flint:icons/remove-all-items")
action.setIcon(icon)
......@@ -698,6 +725,8 @@ class CurvePlotPropertyWidget(qt.QWidget):
action.triggered.connect(self.__removeAllItems)
toolBar.addAction(action)
toolBar.addSeparator()
action = qt.QAction(self)
icon = icons.getQIcon("flint:icons/scan-history")
action.setIcon(icon)
......@@ -709,6 +738,30 @@ class CurvePlotPropertyWidget(qt.QWidget):
return toolBar
def __resetPlotWithOriginalPlot(self):
widget = self.__focusWidget
scan = widget.scan()
plots = scan_info_helper.create_plot_model(scan.scanInfo(), scan)
plots = [p for p in plots if isinstance(p, plot_item_model.CurvePlot)]
if len(plots) == 0:
_logger.warning("No curve plot to display")
qt.QMessageBox.warning(
None, "Warning", "There was no curve plot in this scan"
)
return
plotModel = plots[0]
previousPlotModel = self.__plotModel
# Reuse only available values
if isinstance(previousPlotModel, plot_item_model.CurvePlot):
model_helper.removeNotAvailableChannels(previousPlotModel, plotModel, scan)
model_helper.copyItemsFromChannelNames(
previousPlotModel, plotModel, scan=None
)
if plotModel.styleStrategy() is None:
plotModel.setStyleStrategy(DefaultStyleStrategy(self.__flintModel))
widget.setPlotModel(plotModel)
def __requestLoadScanFromHistory(self):
from bliss.flint.widgets.scan_history_dialog import ScanHistoryDialog
......
......@@ -191,9 +191,11 @@ class RemovePlotItemButton(qt.QToolButton):
def __requestRemoveItem(self):
plotItem = self.__plotItem
plot = plotItem.plot()
if plot is not None:
model_helper.removeItemAndKeepAxes(plot, plotItem)
plotModel = plotItem.plot()
if plotModel is not None:
model_helper.removeItemAndKeepAxes(plotModel, plotItem)
# FIXME: It would be better to make it part of the model
plotModel.tagUserEditTime()
def setPlotItem(self, plotItem: plot_model.Item):
self.__plotItem = plotItem
......
......@@ -1385,6 +1385,10 @@ class Scan:
if displayed_channels is not None:
# Contextual display request
display_extra["plotselect"] = displayed_channels
if self.__scan_display._displayed_channels_time is not None:
display_extra[
"plotselect_time"
] = self.__scan_display._displayed_channels_time
displayed_channels = self.__scan_display._pop_next_scan_displayed_channels()
if displayed_channels is not None:
# Structural display request specified for this scan
......
......@@ -30,6 +30,7 @@ class ScanDisplay(ParametersWardrobe):
"_extra_args": [],
"_scan_metadata": {},
"displayed_channels": [],
"_displayed_channels_time": None,
"scan_display_filter_enabled": True,
"restart_flint_if_stucked": False,
},
......@@ -38,6 +39,7 @@ class ScanDisplay(ParametersWardrobe):
"auto",
"motor_position",
"displayed_channels",
"_displayed_channels_time",
"_scan_metadata",
"scan_display_filter_enabled",
"restart_flint_if_stucked",
......
......@@ -706,7 +706,21 @@ def test_remove_channels__no_value():
assert item.yChannel() is None
def test_copy_config_tree():
def test_copy_config_tree__updated_root_item():
"""The destination plot already contains the plotted channel"""
scan = scan_model.Scan()
master1 = scan_model.Device(scan)
channel2 = scan_model.Channel(master1)
channel2.setName("y1")
master2 = scan_model.Device(scan)
channel3 = scan_model.Channel(master2)
channel3.setName("x")
channel4 = scan_model.Channel(master2)
channel4.setName("y2")
channel5 = scan_model.Channel(master2)
channel5.setName("y3")
scan.seal()
source = plot_item_model.CurvePlot()
destination = plot_item_model.CurvePlot()
......@@ -722,15 +736,59 @@ def test_copy_config_tree():
source.addItem(item3)
add_item(source, "x", "y2")
destItem = add_item(destination, "x", "y1")
add_item(destination, "x", "y3")
model_helper.copyItemsFromChannelNames(source, destination)
model_helper.copyItemsFromChannelNames(source, destination, scan)
assert destItem.yAxis() == "right"
items = list(destination.items())
assert len(items) == 5
gaussianItem = [i for i in items if type(i) == plot_state_model.GaussianFitItem]
assert len(gaussianItem) == 1
assert gaussianItem[0].source().source().yChannel().name() == "y1"
def test_copy_config_tree__created_root_item():
"""The destination plot is empty, but it is feed"""
scan = scan_model.Scan()
master1 = scan_model.Device(scan)
channel2 = scan_model.Channel(master1)
channel2.setName("y1")
master2 = scan_model.Device(scan)
channel3 = scan_model.Channel(master2)
channel3.setName("x")
channel4 = scan_model.Channel(master2)
channel4.setName("y2")
channel5 = scan_model.Channel(master2)
channel5.setName("y3")
scan.seal()
source = plot_item_model.CurvePlot()
destination = plot_item_model.CurvePlot()
item = add_item(source, "x", "y1")
item.setYAxis("right")
item2 = plot_state_model.DerivativeItem(source)
item2.setSource(item)
source.addItem(item2)
item3 = plot_state_model.GaussianFitItem(source)
item3.setSource(item2)
source.addItem(item3)
item = add_item(source, "x", "y3")
item = add_item(source, "x", "y4")
model_helper.copyItemsFromChannelNames(source, destination, scan)
destItem = destination.items()[0]
assert destItem.yAxis() == "right"
items = list(destination.items())
assert len(items) == 4
assert type(items[-1]) == plot_state_model.GaussianFitItem
assert items[-1].source() is items[-2]
gaussianItem = [i for i in items if type(i) == plot_state_model.GaussianFitItem]
assert len(gaussianItem) == 1
assert gaussianItem[0].source().source().yChannel().name() == "y1"
def test_update_xaxis():
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment