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

from __future__ import annotations

10
import typing
11
import numpy
12
import logging
13
14
15
16

from silx.gui import qt
from silx.gui import plot as silx_plot

17
18
_logger = logging.getLogger(__name__)

19

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class _DataWidget(qt.QWidget):
    def __init__(self, parent=None):
        super(_DataWidget, self).__init__(parent=parent)
        layout = qt.QVBoxLayout(self)
        self.__silxWidget = self._createSilxWidget(self)
        layout.addWidget(self.__silxWidget)
        self.__dataDict = {}

    def dataDict(self):
        return self.__dataDict

    def silxWidget(self):
        return self.__silxWidget

    def silxPlot(self):
        """Used by the interactive API.

        This have to returns a PlotWidget, that's why it could be not always
        the same as the silx widget.
        """
        return self.__silxWidget

    def _createSilxWidget(self, parent):
        raise NotImplementedError

    def __getattr__(self, name: str):
        silxWidget = self.silxWidget()
        return getattr(silxWidget, name)

49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
    def updateStoredData(self, field, data):
        data_dict = self.dataDict()

        # Data from the network is sometime not writable
        # This make it fail silx for some use cases
        if data is None:
            return None
        if isinstance(data, numpy.ndarray):
            if not data.flags.writeable:
                data = numpy.array(data)

        data_dict[field] = data

    def removeStoredData(self, field):
        data_dict = self.dataDict()
64
        del data_dict[field]
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

    def getStoredData(self, field=None):
        data_dict = self.dataDict()
        if field is None:
            return data_dict
        else:
            return data_dict.get(field, [])

    def clearStoredData(self):
        data_dict = self.dataDict()
        data_dict.clear()

    def clear(self):
        self.clearStoredData()
        self.silxWidget().clear()

81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
    def selectStoredData(self, *names, **kwargs):
        # FIXME: This have to be moved per plot widget
        # FIXME: METHOD have to be removed
        method = self.METHOD
        if "legend" not in kwargs and method.startswith("add"):
            kwargs["legend"] = " -> ".join(names)
        data_dict = self.dataDict()
        args = tuple(data_dict[name] for name in names)
        widget_method = getattr(self, method)
        # Plot
        widget_method(*args, **kwargs)

    def deselectStoredData(self, *names):
        legend = " -> ".join(names)
        self.remove(legend)

97
98

class Plot1D(_DataWidget):
99
100
    """Generic plot to display 1D data"""

101
102
103
    # Name of the method to add data to the plot
    METHOD = "addCurve"

104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
    class CurveItem(typing.NamedTuple):
        xdata: str
        ydata: str
        style: typing.Dict[str, object]

    def __init__(self, parent=None):
        _DataWidget.__init__(self, parent=parent)
        self.__items = {}
        self.__autoUpdatePlot = True
        self.__raiseOnException = False

    def setRaiseOnException(self, raises):
        """To simplify remote debug"""
        self.__raiseOnException = raises

119
    def _createSilxWidget(self, parent):
Valentin Valls's avatar
Valentin Valls committed
120
121
122
        widget = silx_plot.Plot1D(parent=parent)
        widget.setDataMargins(0.05, 0.05, 0.05, 0.05)
        return widget
123

124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
    def setAutoUpdatePlot(self, update="bool"):
        """Set to true to enable or disable update of plot for each changes of
        the data or items"""
        self.__autoUpdatePlot = update

    def clearItems(self):
        """Remove the item definitions"""
        self.__items.clear()
        self.__updatePlotIfNeeded()

    def removeItem(self, legend: str):
        """Remove a specific item by name"""
        del self.__items[legend]
        self.__updatePlotIfNeeded()

    def addCurveItem(self, xdata: str, ydata: str, legend: str = None, **kwargs):
        """Define an item which have to be displayed with the specified data
        name
        """
        if legend is None:
            legend = ydata + " -> " + xdata
        self.__items[legend] = self.CurveItem(xdata, ydata, kwargs)
        self.__updatePlotIfNeeded()

    def setData(self, **kwargs):
        dataDict = self.dataDict()
        for k, v in kwargs.items():
            dataDict[k] = v
        self.__updatePlotIfNeeded()

    def appendData(self, **kwargs):
        dataDict = self.dataDict()
        for k, v in kwargs.items():
            d = dataDict.get(k, None)
            if d is None:
                d = v
            else:
                d = numpy.concatenate((d, v))
            dataDict[k] = d
        self.__updatePlotIfNeeded()

    def clear(self):
        super(Plot1D, self).clear()
        self.__updatePlotIfNeeded()

    def updatePlot(self, resetzoom: bool = True):
        try:
            self.__updatePlot()
        except Exception:
            _logger.error("Error while updating the plot", exc_info=True)
            if self.__raiseOnException:
                raise
        if resetzoom:
            self.resetZoom()

    def __updatePlotIfNeeded(self):
        if self.__autoUpdatePlot:
            self.updatePlot(resetzoom=True)

    def __updatePlot(self):
        plot = self.silxPlot()
        plot.clear()
        dataDict = self.dataDict()
        for legend, item in self.__items.items():
            xData = dataDict.get(item.xdata)
            yData = dataDict.get(item.ydata)
            if xData is None or yData is None:
                continue
            if len(yData) != len(xData):
                size = min(len(yData), len(xData))
                xData = xData[0:size]
                yData = yData[0:size]
            if len(yData) == 0:
                continue
            plot.addCurve(xData, yData, legend=legend, **item.style, resetzoom=False)

200

201
class Plot2D(_DataWidget):
202
203
    """Generic plot to display 2D data"""

204
205
206
    # Name of the method to add data to the plot
    METHOD = "addImage"

207
    def _createSilxWidget(self, parent):
Valentin Valls's avatar
Valentin Valls committed
208
209
210
        widget = silx_plot.Plot2D(parent=parent)
        widget.setDataMargins(0.05, 0.05, 0.05, 0.05)
        return widget
211

212
213
214
215
    def setDisplayedIntensityHistogram(self, show):
        self.getIntensityHistogramAction().setVisible(show)


216
class ImageView(_DataWidget):
217
218
    """Dedicated plot to display an image"""

219
220
221
    # Name of the method to add data to the plot
    METHOD = "setImage"

222
    def _createSilxWidget(self, parent):
Valentin Valls's avatar
Valentin Valls committed
223
224
225
        widget = silx_plot.ImageView(parent=parent)
        widget.setDataMargins(0.05, 0.05, 0.05, 0.05)
        return widget
226

227
228
229
230
    def setDisplayedIntensityHistogram(self, show):
        self.getIntensityHistogramAction().setVisible(show)


231
class ScatterView(_DataWidget):
232
233
    """Dedicated plot to display a 2D scatter"""

234
235
236
    # Name of the method to add data to the plot
    METHOD = "setData"

237
    def _createSilxWidget(self, parent):
Valentin Valls's avatar
Valentin Valls committed
238
239
240
241
        widget = silx_plot.ScatterView(parent=parent)
        plot = widget.getPlotWidget()
        plot.setDataMargins(0.05, 0.05, 0.05, 0.05)
        return widget
242

243
    def getDataRange(self):
244
        plot = self.silxWidget().getPlotWidget()
245
246
        return plot.getDataRange()

247
248
249
    def clear(self):
        self.silxWidget().setData(None, None, None)

250
251
252
    def setData(
        self, x, y, value, xerror=None, yerror=None, alpha=None, resetzoom=True
    ):
253
        self.silxWidget().setData(
254
255
256
257
258
259
260
            x, y, value, xerror=xerror, yerror=yerror, alpha=alpha, copy=False
        )
        if resetzoom:
            # Else the view is not updated
            self.resetZoom()


261
class StackImageView(_DataWidget):
262
263
    """Dedicated plot to display a stack of images"""

264
265
266
    # Name of the method to add data to the plot
    METHOD = "setStack"

267
268
269
    def _createSilxWidget(self, parent):
        return silx_plot.StackView(parent=parent)

270
    def getDataRange(self):
271
        plot = self.silxWidget().getPlotWidget()
272
        return plot.getDataRange()