Commit 81677966 authored by Matias Guijarro's avatar Matias Guijarro
Browse files

Merge branch 'ttl-for-regulation' into 'master'

Flint: Create a dedicated TTL widget for the regulation

Closes #3372

See merge request !4672
parents fde9ac9b acdb1251
Pipeline #75289 failed with stages
in 145 minutes and 58 seconds
...@@ -9,6 +9,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ...@@ -9,6 +9,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Flint
- Added widget in the regulation plot to specify a time to life for the data
### Changed
- Flint
- Changed time to life of regulation plot is now independent to the x-axis
selected duration
### Fixed
### Removed
## [1.10.1]
### Added
- Flint - Flint
- Added FFT tool for curve plots - Added FFT tool for curve plots
- Added marker API for live image plots - Added marker API for live image plots
......
...@@ -576,7 +576,12 @@ class TimeCurvePlot(BasePlot): ...@@ -576,7 +576,12 @@ class TimeCurvePlot(BasePlot):
""" """
self.submit("setXName", name) self.submit("setXName", name)
def select_x_duration(self, second: int): @property
def xaxis_duration(self):
return self.submit("xDuration")
@xaxis_duration.setter
def xaxis_duration(self, second: int):
""" """
Select the x-axis duration in second Select the x-axis duration in second
...@@ -585,6 +590,32 @@ class TimeCurvePlot(BasePlot): ...@@ -585,6 +590,32 @@ class TimeCurvePlot(BasePlot):
""" """
self.submit("setXDuration", second) self.submit("setXDuration", second)
def select_x_duration(self, second: int):
"""
Select the x-axis duration in second
Arguments:
second: Amount of seconds displayed in the x-axis
"""
self.xaxis_duration = second
@property
def ttl(self):
return self.submit("ttl")
@ttl.setter
def ttl(self, second: int):
"""
Set the time to live of the data.
After this period of time, a received data is not anymore displayable
in Flint.
Arguments:
second: Amount of seconds a data will live
"""
self.submit("setTtl", second)
def add_time_curve_item(self, yname, **kwargs): def add_time_curve_item(self, yname, **kwargs):
""" """
Select a dedicated data to be displayed against the time. Select a dedicated data to be displayed against the time.
......
...@@ -11,88 +11,14 @@ import logging ...@@ -11,88 +11,14 @@ import logging
import numpy import numpy
from silx.gui import qt from silx.gui import qt
from silx.gui import icons
from silx.gui.plot import Plot1D from silx.gui.plot import Plot1D
from silx.gui.plot.items import axis as axis_mdl from silx.gui.plot.items import axis as axis_mdl
from bliss.flint.widgets.utils.duration_action import DurationAction
from bliss.flint.widgets.utils.static_icon import StaticIcon
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class DurationAction(qt.QAction):
valueChanged = qt.Signal(float)
def __init__(self):
super(DurationAction, self).__init__()
self.__duration = None
self.__durations = {}
self.__menu = qt.QMenu()
self.__menu.aboutToShow.connect(self.__menuAboutToShow)
self.setMenu(self.__menu)
def __menuAboutToShow(self):
menu = self.sender()
menu.clear()
currentDuration = self.__duration
currentWasFound = False
group = qt.QActionGroup(menu)
group.setExclusive(True)
for value, (label, icon) in self.__durations.items():
action = qt.QAction()
action.setText(label)
action.setData(value)
action.setIcon(icon)
action.setCheckable(True)
if currentDuration == value:
action.setChecked(True)
currentWasFound = True
group.addAction(action)
menu.addAction(action)
if currentDuration is not None and not currentWasFound:
menu.addSeparator()
action = qt.QAction()
action.setText(f"{currentDuration}s")
action.setData(currentDuration)
action.setCheckable(True)
action.setChecked(True)
currentWasFound = True
group.addAction(action)
menu.addAction(action)
group.triggered.connect(self.__actionSelected)
def __actionSelected(self, action):
duration = action.data()
self.setDuration(duration)
def setDuration(self, duration):
if self.__duration == duration:
return
self.__duration = duration
self.__updateLookAndFeel()
self.valueChanged.emit(duration)
def addDuration(self, label, value, icon):
if isinstance(icon, str):
icon = icons.getQIcon(icon)
self.__durations[value] = label, icon
def duration(self):
"""Return a duration in second"""
return self.__duration
def __updateLookAndFeel(self):
duration = self.__duration
label, icon = self.__durations.get(duration, (None, None))
if icon is None:
icon = icons.getQIcon("flint:icons/duration-x")
if label is None:
label = f"{duration}s"
self.setToolTip(f"Duration of {label} selected")
self.setIcon(icon)
class TimeCurvePlot(qt.QWidget): class TimeCurvePlot(qt.QWidget):
"""Curve plot which handle data following the time """Curve plot which handle data following the time
...@@ -110,20 +36,33 @@ class TimeCurvePlot(qt.QWidget): ...@@ -110,20 +36,33 @@ class TimeCurvePlot(qt.QWidget):
layout = qt.QVBoxLayout(self) layout = qt.QVBoxLayout(self)
layout.addWidget(self.__plot) layout.addWidget(self.__plot)
self.__duration = 60 * 2 self.__xduration = 60 * 2
self.__ttl = 60 * 5
self.__durationAction = DurationAction()
self.__durationAction.setCheckable(True) self.__xdurationAction = DurationAction(self)
self.__durationAction.setChecked(True) self.__xdurationAction.setCheckable(True)
self.__durationAction.addDuration("1h", 60 * 60, "flint:icons/duration-1h") self.__xdurationAction.setChecked(True)
self.__durationAction.addDuration("30m", 30 * 60, "flint:icons/duration-30m") self.__xdurationAction.addDuration("1h", 60 * 60)
self.__durationAction.addDuration("10m", 10 * 60, "flint:icons/duration-10m") self.__xdurationAction.addDuration("30m", 30 * 60)
self.__durationAction.addDuration("5m", 5 * 60, "flint:icons/duration-5m") self.__xdurationAction.addDuration("10m", 10 * 60)
self.__durationAction.addDuration("2m", 2 * 60, "flint:icons/duration-2m") self.__xdurationAction.addDuration("5m", 5 * 60)
self.__durationAction.addDuration("1m", 1 * 60, "flint:icons/duration-1m") self.__xdurationAction.addDuration("2m", 2 * 60)
self.__durationAction.addDuration("30s", 30, "flint:icons/duration-30s") self.__xdurationAction.addDuration("1m", 1 * 60)
self.__durationAction.setDuration(self.__duration) self.__xdurationAction.addDuration("30s", 30)
self.__durationAction.valueChanged.connect(self.__durationChanged) self.__xdurationAction.setDuration(self.__xduration)
self.__xdurationAction.valueChanged.connect(self.__xdurationChanged)
# time to live widget
self.__ttlAction = DurationAction(self)
self.__ttlAction.addDuration("1h", 60 * 60)
self.__ttlAction.addDuration("30m", 30 * 60)
self.__ttlAction.addDuration("10m", 10 * 60)
self.__ttlAction.addDuration("5m", 5 * 60)
self.__ttlAction.addDuration("2m", 2 * 60)
self.__ttlAction.addDuration("1m", 1 * 60)
self.__ttlAction.addDuration("30s", 30)
self.__ttlAction.setDuration(self.__ttl)
self.__ttlAction.valueChanged.connect(self.__ttlChanged)
self.__plot.setGraphXLabel("Time") self.__plot.setGraphXLabel("Time")
xAxis = self.__plot.getXAxis() xAxis = self.__plot.getXAxis()
...@@ -137,22 +76,48 @@ class TimeCurvePlot(qt.QWidget): ...@@ -137,22 +76,48 @@ class TimeCurvePlot(qt.QWidget):
# FIXME: The toolbar have to be recreated, not updated # FIXME: The toolbar have to be recreated, not updated
toolbar = self.__plot.toolBar() toolbar = self.__plot.toolBar()
xAutoAction = self.__plot.getXAxisAutoScaleAction() xAutoAction = self.__plot.getXAxisAutoScaleAction()
toolbar.insertAction(xAutoAction, self.__durationAction) toolbar.insertAction(xAutoAction, self.__xdurationAction)
xAutoAction.setVisible(False) xAutoAction.setVisible(False)
xLogAction = self.__plot.getXAxisLogarithmicAction() xLogAction = self.__plot.getXAxisLogarithmicAction()
xLogAction.setVisible(False) xLogAction.setVisible(False)
timeToolbar = qt.QToolBar(self)
ttlIconWidget = StaticIcon(self)
ttlIconWidget.setIcon("flint:icons/ttl-static")
ttlIconWidget.setToolTip("Define the time to live of the data")
ttlIconWidget.redirectClickTo(self.__ttlAction)
timeToolbar.addWidget(ttlIconWidget)
timeToolbar.addAction(self.__ttlAction)
self._timeToolbar = timeToolbar
self.__plot.addToolBar(timeToolbar)
self.clear() self.clear()
def __durationChanged(self, duration): def __xdurationChanged(self, duration):
self.setXDuration(duration) self.setXDuration(duration)
def xDuration(self):
return self.__xduration
def setXDuration(self, duration): def setXDuration(self, duration):
self.__durationAction.setDuration(duration) self.__xdurationAction.setDuration(duration)
self.__duration = duration self.__xduration = duration
if self.__ttl < duration:
self.setTtl(duration)
self.__safeUpdatePlot()
def ttl(self):
return self.__ttl
def setTtl(self, duration):
self.__ttlAction.setDuration(duration)
self.__ttl = duration
self.__dropOldData() self.__dropOldData()
self.__safeUpdatePlot() self.__safeUpdatePlot()
def __ttlChanged(self, duration):
self.setTtl(duration)
def __dropOldData(self): def __dropOldData(self):
xData = self.__data.get(self.__xAxisName) xData = self.__data.get(self.__xAxisName)
if xData is None: if xData is None:
...@@ -160,14 +125,12 @@ class TimeCurvePlot(qt.QWidget): ...@@ -160,14 +125,12 @@ class TimeCurvePlot(qt.QWidget):
if len(xData) == 0: if len(xData) == 0:
return return
duration = xData[-1] - xData[0] duration = xData[-1] - xData[0]
if duration <= self.__duration: if duration <= self.__ttl:
return return
# FIXME: most of the time only last items with be removed # FIXME: most of the time only last items with be removed
# There is maybe no need to recompute the whole array # There is maybe no need to recompute the whole array
distFromLastValueOfView = self.__duration - numpy.abs( distFromLastValueOfView = self.__ttl - numpy.abs(xData[-1] - self.__ttl - xData)
xData[-1] - self.__duration - xData
)
index = numpy.argmax(distFromLastValueOfView) index = numpy.argmax(distFromLastValueOfView)
if index >= 1: if index >= 1:
index = index - 1 index = index - 1
...@@ -233,12 +196,12 @@ class TimeCurvePlot(qt.QWidget): ...@@ -233,12 +196,12 @@ class TimeCurvePlot(qt.QWidget):
self.__safeUpdatePlot() self.__safeUpdatePlot()
def resetZoom(self): def resetZoom(self):
if self.__durationAction.isChecked(): if self.__xdurationAction.isChecked():
self.__plot.resetZoom() self.__plot.resetZoom()
xData = self.__data.get(self.__xAxisName) xData = self.__data.get(self.__xAxisName)
if xData is not None and len(xData) > 0: if xData is not None and len(xData) > 0:
xmax = xData[-1] xmax = xData[-1]
xmin = xmax - self.__duration xmin = xmax - self.__xduration
xAxis = self.__plot.getXAxis() xAxis = self.__plot.getXAxis()
xAxis.setLimits(xmin, xmax) xAxis.setLimits(xmin, xmax)
......
<?xml version="1.0" encoding="UTF-8"?>
<svg id="svg39" version="1.1" viewBox="0 0 32 32" xml:space="preserve" 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="metadata43"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><path id="path822-9" d="m4.9221 6.5357c0.042943 6.4507-0.11128 13.016 0 19.404 0.41254 1.5614 5.2398 2.7715 11.057 2.7715 5.8123-0.0017 10.68-1.2113 11.092-2.7715 0.09522-6.3604 0-12.96 0-19.404" fill="none" stroke="#8e8e8e" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2.5"/><ellipse id="path822" cx="16.003" cy="6.2716" rx="11.115" ry="2.9817" fill="none" stroke="#8e8e8e" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2.5" style="font-variant-east_asian:normal"/><g id="text863" stroke-width=".1998" aria-label="TTL"><path id="path817" d="m8.0413 14.812h5.3696v1.1356h-1.9316v4.6906h-1.5024v-4.6906h-1.9355z"/><path id="path819" d="m13.676 14.812h5.3696v1.1356h-1.9316v4.6906h-1.5024v-4.6906h-1.9355z"/><path id="path821" d="m19.818 14.812h1.5024v4.6906h2.638v1.1356h-4.1403z"/></g>
</svg>
# -*- coding: utf-8 -*-
#
# This file is part of the bliss project
#
# Copyright (c) 2015-2022 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.
from __future__ import annotations
import typing
from silx.gui import qt
from silx.gui import icons
class DurationAction(qt.QAction):
valueChanged = qt.Signal(float)
DEFAULT_ICONS = {
60 * 60: "flint:icons/duration-1h",
30 * 60: "flint:icons/duration-30m",
10 * 60: "flint:icons/duration-10m",
5 * 60: "flint:icons/duration-5m",
2 * 60: "flint:icons/duration-2m",
1 * 60: "flint:icons/duration-1m",
30: "flint:icons/duration-30s",
}
def __init__(self, parent=None):
super(DurationAction, self).__init__(parent)
self.__duration = None
self.__durations = {}
self.__menu = qt.QMenu(parent)
self.__menu.aboutToShow.connect(self.__menuAboutToShow)
self.setMenu(self.__menu)
def __menuAboutToShow(self):
menu = self.sender()
menu.clear()
currentDuration = self.__duration
currentWasFound = False
group = qt.QActionGroup(menu)
group.setExclusive(True)
for value, (label, icon) in self.__durations.items():
action = qt.QAction()
action.setText(label)
action.setData(value)
action.setIcon(icon)
action.setCheckable(True)
if currentDuration == value:
action.setChecked(True)
currentWasFound = True
group.addAction(action)
menu.addAction(action)
if currentDuration is not None and not currentWasFound:
menu.addSeparator()
action = qt.QAction()
action.setText(f"{currentDuration}s")
action.setData(currentDuration)
action.setCheckable(True)
action.setChecked(True)
currentWasFound = True
group.addAction(action)
menu.addAction(action)
group.triggered.connect(self.__actionSelected)
def __actionSelected(self, action):
duration = action.data()
self.setDuration(duration)
def setDuration(self, duration):
if self.__duration == duration:
return
self.__duration = duration
self.__updateLookAndFeel()
self.valueChanged.emit(duration)
def addDuration(
self, label: str, value: float, icon: typing.Union[str, qt.QIcon] = None
):
"""Add a selectable duration in second
Attributes:
label: Text to display with the item
value: Duration in second
icon: silx icon id to display with the item, else a default icon is
loaded.
"""
if icon is None:
icon = self.DEFAULT_ICONS[value]
if isinstance(icon, str):
icon = icons.getQIcon(icon)
self.__durations[value] = label, icon
def duration(self) -> float:
"""Return a duration in second"""
return self.__duration
def __updateLookAndFeel(self):
duration = self.__duration
label, icon = self.__durations.get(duration, (None, None))
if icon is None:
icon = icons.getQIcon("flint:icons/duration-x")
if label is None:
label = f"{duration}s"
self.setToolTip(f"Duration of {label} selected")
self.setIcon(icon)
# -*- coding: utf-8 -*-
#
# This file is part of the bliss project
#
# Copyright (c) 2015-2022 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.
from __future__ import annotations
import typing
from silx.gui import qt
from silx.gui import icons
class StaticIcon(qt.QLabel):
clicked = qt.Signal()
def __init__(self, parent=None):
super(StaticIcon, self).__init__(parent=parent)
self.__targetAction = None
def setIcon(self, icon: typing.Union[str, qt.QIcon]):
"""Set an icon
Arguments:
icon: This can be a QIcon or a silx resource name.
"""
if isinstance(icon, str):
icon = icons.getQIcon(icon)
# FIXME: Maybe the icon size could be read from the parent
pixmap = icon.pixmap(qt.QSize(24, 24))
self.setPixmap(pixmap)
def event(self, event: qt.QEvent):
if event.type() == qt.QEvent.MouseButtonRelease:
self.__redirectClick()
self.clicked.emit()
return qt.QLabel.event(self, event)
def __redirectClick(self):
if self.__targetAction is None:
return
action = self.__targetAction
menu = action.menu()
if menu:
action = menu.menuAction()
# pos like it is drop by the next widget in the toolbar
# this is obviously not always the case
pos = self.mapToGlobal(qt.QPoint(self.width(), self.height()))
menu.popup(pos)
else:
action.trigger()
def redirectClickTo(self, action):
"""Define the action which will be triggered when the icon is clicked."""
self.__targetAction = action
...@@ -537,8 +537,8 @@ def test_time_curve_plot(flint_session): ...@@ -537,8 +537,8 @@ def test_time_curve_plot(flint_session):
assert vrange[0] == [0, 3] assert vrange[0] == [0, 3]
assert vrange[1] == [0, 6] assert vrange[1] == [0, 6]
# when a fixed duration is used, the data disappear on one side # when a fixed duration is used, the oldest data disappear
p.select_x_duration(second=5) p.ttl = 5
p.append_data(time=[10], diode1=[2], diode2=[6]) p.append_data(time=[10], diode1=[2], diode2=[6])
vrange = p.get_data_range() vrange = p.get_data_range()
assert vrange[0][0] > 1 assert vrange[0][0] > 1
......
...@@ -76,7 +76,7 @@ def test_time_curve_plot__drop_data(time_curve_plot_widget): ...@@ -76,7 +76,7 @@ def test_time_curve_plot__drop_data(time_curve_plot_widget):
on the other side. on the other side.
""" """
w = time_curve_plot_widget w = time_curve_plot_widget
w.setXDuration(5) w.setTtl(5)
w.addTimeCurveItem("value1") w.addTimeCurveItem("value1")
w.addTimeCurveItem("value2") w.addTimeCurveItem("value2")
w.appendData(time=[0, 1, 2], value1=[0, 1, 2], value2=[0, 1, 2]) w.appendData(time=[0, 1, 2], value1=[0, 1, 2], value2=[0, 1, 2])
...@@ -87,3 +87,26 @@ def test_time_curve_plot__drop_data(time_curve_plot_widget): ...@@ -87,3 +87,26 @@ def test_time_curve_plot__drop_data(time_curve_plot_widget):
assert len(curve.getXData()) <= 5 + 2 assert len(curve.getXData()) <= 5 + 2
assert curve.getXData()[0] > 0 assert curve.getXData()[0] > 0
assert curve.getXData()[-1] == 8 assert curve.getXData()[-1] == 8
def test_time_curve_plot__limited_view(time_curve_plot_widget):
"""Create a plot with limited x-axis duration and feed it with data
We expect the plot to contain more items data than what it is displayed.
"""
w = time_curve_plot_widget
w.setXDuration(5)
w.setTtl(10)
w.addTimeCurveItem("value1")
w.addTimeCurveItem("value2")
w.appendData(time=[0, 1, 2], value1=[0, 1, 2], value2=[0, 1, 2])
w.appendData(time=[3, 4, 5], value1=[0, 1, 2], value2=[0, 1, 2])
w.appendData(time=[6, 7, 8], value1=[0, 1, 2], value2=[0, 1, 2])
plot = w.getPlotWidget()
xAxis = plot.getXAxis()
assert xAxis.getLimits()[0] > 0
assert xAxis.getLimits()[1] == 8.0
curve = plot.getAllCurves()[0]
assert len(curve.getXData()) == 9
assert curve.getXData()[0] == 0
assert curve.getXData()[-1] == 8
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