time_curve_plot.py 7.32 KB
Newer Older
1
2
3
4
# -*- coding: utf-8 -*-
#
# This file is part of the bliss project
#
5
# Copyright (c) 2015-2022 Beamline Control Unit, ESRF
6
7
8
9
10
11
12
13
14
# Distributed under the GNU LGPLv3. See LICENSE for more info.

from __future__ import annotations

import logging
import numpy

from silx.gui import qt
from silx.gui.plot import Plot1D
Valentin Valls's avatar
Valentin Valls committed
15
from silx.gui.plot.items import axis as axis_mdl
16
from bliss.flint.widgets.utils.duration_action import DurationAction
17
from bliss.flint.widgets.utils.static_icon import StaticIcon
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

_logger = logging.getLogger(__name__)


class TimeCurvePlot(qt.QWidget):
    """Curve plot which handle data following the time

    - The X is supposed to be the epoch time
    - The data can be appended
    - The user can choose the amount of time to watch
    """

    def __init__(self, parent=None):
        super(TimeCurvePlot, self).__init__(parent=parent)
        self.__data = {}
        self.__description = {}
        self.__xAxisName = "time"
        self.__plot = Plot1D(self)
        layout = qt.QVBoxLayout(self)
        layout.addWidget(self.__plot)

39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
        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)
Valentin Valls's avatar
Valentin Valls committed
66

67
68
69
70
71
72
73
74
75
        self.__plot.setGraphXLabel("Time")
        xAxis = self.__plot.getXAxis()
        xAxis.setTickMode(axis_mdl.TickMode.TIME_SERIES)
        xAxis.setTimeZone(None)

        self.__plot.setDataMargins(
            xMinMargin=0.0, xMaxMargin=0.0, yMinMargin=0.1, yMaxMargin=0.1
        )

76
77
78
        # FIXME: The toolbar have to be recreated, not updated
        toolbar = self.__plot.toolBar()
        xAutoAction = self.__plot.getXAxisAutoScaleAction()
79
        toolbar.insertAction(xAutoAction, self.__xdurationAction)
80
81
82
83
        xAutoAction.setVisible(False)
        xLogAction = self.__plot.getXAxisLogarithmicAction()
        xLogAction.setVisible(False)

84
85
86
87
88
89
90
91
92
93
        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)

94
95
        self.clear()

96
    def __xdurationChanged(self, duration):
97
98
        self.setXDuration(duration)

99
100
101
    def xDuration(self):
        return self.__xduration

102
    def setXDuration(self, duration):
103
104
105
106
107
108
109
110
111
112
113
114
        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
115
116
117
        self.__dropOldData()
        self.__safeUpdatePlot()

118
119
120
    def __ttlChanged(self, duration):
        self.setTtl(duration)

121
122
123
124
125
126
127
    def __dropOldData(self):
        xData = self.__data.get(self.__xAxisName)
        if xData is None:
            return
        if len(xData) == 0:
            return
        duration = xData[-1] - xData[0]
128
        if duration <= self.__ttl:
129
130
131
132
            return

        # FIXME: most of the time only last items with be removed
        # There is maybe no need to recompute the whole array
133
        distFromLastValueOfView = self.__ttl - numpy.abs(xData[-1] - self.__ttl - xData)
134
135
136
137
138
139
140
141
142
        index = numpy.argmax(distFromLastValueOfView)
        if index >= 1:
            index = index - 1
        if index == 0:
            # early skip
            return
        for name, data in self.__data.items():
            data = data[index:]
            self.__data[name] = data
143

144
145
146
147
148
149
    def getDataRange(self):
        r = self.__plot.getDataRange()
        if r is None:
            return None
        return r[0], r[1]

150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
    def setGraphGrid(self, which):
        self.__plot.setGraphGrid(which)

    def setGraphTitle(self, title: str):
        self.__plot.setGraphTitle(title)

    def setGraphXLabel(self, label: str):
        self.__plot.setGraphXLabel(label)

    def setGraphYLabel(self, label: str, axis="left"):
        self.__plot.setGraphYLabel(label, axis=axis)

    def getPlotWidget(self):
        return self.__plot

    def clear(self):
        self.__data = {}
        self.__plot.clear()

    def __appendData(self, name, newData):
        if name in self.__data:
            data = self.__data[name]
            data = numpy.concatenate((data, newData))
        else:
            data = newData
        self.__data[name] = data

177
    def addTimeCurveItem(self, yName, **kwargs):
178
179
180
181
182
183
184
185
186
187
188
189
190
        """Update the plot description"""
        self.__description[yName] = kwargs
        self.__safeUpdatePlot()

    def setXName(self, name):
        """Update the name used as X axis"""
        self.__xAxisName = name
        self.__safeUpdatePlot()

    def setData(self, **kwargs):
        self.__data = dict(kwargs)
        self.__safeUpdatePlot()

Valentin Valls's avatar
Valentin Valls committed
191
192
193
194
    def appendData(self, **kwargs):
        """Update the current data with extra data"""
        for name, data in kwargs.items():
            self.__appendData(name, data)
195
        self.__dropOldData()
Valentin Valls's avatar
Valentin Valls committed
196
197
        self.__safeUpdatePlot()

198
    def resetZoom(self):
199
        if self.__xdurationAction.isChecked():
200
201
202
203
            self.__plot.resetZoom()
            xData = self.__data.get(self.__xAxisName)
            if xData is not None and len(xData) > 0:
                xmax = xData[-1]
204
                xmin = xmax - self.__xduration
205
206
207
                xAxis = self.__plot.getXAxis()
                xAxis.setLimits(xmin, xmax)

208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
    def __safeUpdatePlot(self):
        try:
            self.__updatePlot()
        except Exception:
            _logger.critical("Error while updating the plot", exc_info=True)

    def __updatePlot(self):
        self.__plot.clear()
        xData = self.__data.get(self.__xAxisName)
        if xData is None:
            return
        for name, style in self.__description.items():
            yData = self.__data.get(name)
            if yData is None:
                continue
            if "legend" not in style:
                style["legend"] = name
            style["resetzoom"] = False
            self.__plot.addCurve(xData, yData, **style)
227
        self.resetZoom()