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
### 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
- Added FFT tool for curve plots
- Added marker API for live image plots
......
......@@ -576,7 +576,12 @@ class TimeCurvePlot(BasePlot):
"""
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
......@@ -585,6 +590,32 @@ class TimeCurvePlot(BasePlot):
"""
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):
"""
Select a dedicated data to be displayed against the time.
......
......@@ -11,88 +11,14 @@ import logging
import numpy
from silx.gui import qt
from silx.gui import icons
from silx.gui.plot import Plot1D
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__)
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):
"""Curve plot which handle data following the time
......@@ -110,20 +36,33 @@ class TimeCurvePlot(qt.QWidget):
layout = qt.QVBoxLayout(self)
layout.addWidget(self.__plot)
self.__duration = 60 * 2
self.__durationAction = DurationAction()
self.__durationAction.setCheckable(True)
self.__durationAction.setChecked(True)
self.__durationAction.addDuration("1h", 60 * 60, "flint:icons/duration-1h")
self.__durationAction.addDuration("30m", 30 * 60, "flint:icons/duration-30m")
self.__durationAction.addDuration("10m", 10 * 60, "flint:icons/duration-10m")
self.__durationAction.addDuration("5m", 5 * 60, "flint:icons/duration-5m")
self.__durationAction.addDuration("2m", 2 * 60, "flint:icons/duration-2m")
self.__durationAction.addDuration("1m", 1 * 60, "flint:icons/duration-1m")
self.__durationAction.addDuration("30s", 30, "flint:icons/duration-30s")
self.__durationAction.setDuration(self.__duration)
self.__durationAction.valueChanged.connect(self.__durationChanged)
self.__xduration = 60 * 2
self.__ttl = 60 * 5
self.__xdurationAction = DurationAction(self)
self.__xdurationAction.setCheckable(True)
self.__xdurationAction.setChecked(True)
self.__xdurationAction.addDuration("1h", 60 * 60)
self.__xdurationAction.addDuration("30m", 30 * 60)
self.__xdurationAction.addDuration("10m", 10 * 60)
self.__xdurationAction.addDuration("5m", 5 * 60)
self.__xdurationAction.addDuration("2m", 2 * 60)
self.__xdurationAction.addDuration("1m", 1 * 60)
self.__xdurationAction.addDuration("30s", 30)
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")
xAxis = self.__plot.getXAxis()
......@@ -137,22 +76,48 @@ class TimeCurvePlot(qt.QWidget):
# FIXME: The toolbar have to be recreated, not updated
toolbar = self.__plot.toolBar()
xAutoAction = self.__plot.getXAxisAutoScaleAction()
toolbar.insertAction(xAutoAction, self.__durationAction)
toolbar.insertAction(xAutoAction, self.__xdurationAction)
xAutoAction.setVisible(False)
xLogAction = self.__plot.getXAxisLogarithmicAction()
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()
def __durationChanged(self, duration):
def __xdurationChanged(self, duration):
self.setXDuration(duration)
def xDuration(self):
return self.__xduration
def setXDuration(self, duration):
self.__durationAction.setDuration(duration)
self.__duration = duration
self.__xdurationAction.setDuration(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.__safeUpdatePlot()
def __ttlChanged(self, duration):
self.setTtl(duration)
def __dropOldData(self):
xData = self.__data.get(self.__xAxisName)
if xData is None:
......@@ -160,14 +125,12 @@ class TimeCurvePlot(qt.QWidget):
if len(xData) == 0:
return
duration = xData[-1] - xData[0]
if duration <= self.__duration:
if duration <= self.__ttl:
return
# FIXME: most of the time only last items with be removed
# There is maybe no need to recompute the whole array
distFromLastValueOfView = self.__duration - numpy.abs(
xData[-1] - self.__duration - xData
)
distFromLastValueOfView = self.__ttl - numpy.abs(xData[-1] - self.__ttl - xData)
index = numpy.argmax(distFromLastValueOfView)
if index >= 1:
index = index - 1
......@@ -233,12 +196,12 @@ class TimeCurvePlot(qt.QWidget):
self.__safeUpdatePlot()
def resetZoom(self):
if self.__durationAction.isChecked():
if self.__xdurationAction.isChecked():
self.__plot.resetZoom()
xData = self.__data.get(self.__xAxisName)
if xData is not None and len(xData) > 0:
xmax = xData[-1]
xmin = xmax - self.__duration
xmin = xmax - self.__xduration
xAxis = self.__plot.getXAxis()
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):
assert vrange[0] == [0, 3]
assert vrange[1] == [0, 6]
# when a fixed duration is used, the data disappear on one side
p.select_x_duration(second=5)
# when a fixed duration is used, the oldest data disappear
p.ttl = 5
p.append_data(time=[10], diode1=[2], diode2=[6])
vrange = p.get_data_range()
assert vrange[0][0] > 1
......
......@@ -76,7 +76,7 @@ def test_time_curve_plot__drop_data(time_curve_plot_widget):
on the other side.
"""
w = time_curve_plot_widget
w.setXDuration(5)
w.setTtl(5)
w.addTimeCurveItem("value1")
w.addTimeCurveItem("value2")
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):
assert len(curve.getXData()) <= 5 + 2
assert curve.getXData()[0] > 0
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