summaryrefslogtreecommitdiff
path: root/silx/gui/data/DataViewer.py
blob: 4db2863999400d13815344a9cbed90531de6e1af (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ###########################################################################*/
"""This module defines a widget designed to display data using the most adapted
view from the ones provided by silx.
"""
from __future__ import division

from silx.gui.data import DataViews
from silx.gui.data.DataViews import _normalizeData
import logging
from silx.gui import qt
from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector
from silx.utils import deprecation
from silx.utils.property import classproperty

__authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "24/04/2018"


_logger = logging.getLogger(__name__)


class DataViewer(qt.QFrame):
    """Widget to display any kind of data

    .. image:: img/DataViewer.png

    The method :meth:`setData` allows to set any data to the widget. Mostly
    `numpy.array` and `h5py.Dataset` are supported with adapted views. Other
    data types are displayed using a text viewer.

    A default view is automatically selected when a data is set. The method
    :meth:`setDisplayMode` allows to change the view. To have a graphical tool
    to select the view, prefer using the widget :class:`DataViewerFrame`.

    The dimension of the input data and the expected dimension of the selected
    view can differ. For example you can display an image (2D) from 4D
    data. In this case a :class:`NumpyAxesSelector` is displayed to allow the
    user to select the axis mapping and the slicing of other axes.

    .. code-block:: python

        import numpy
        data = numpy.random.rand(500,500)
        viewer = DataViewer()
        viewer.setData(data)
        viewer.setVisible(True)
    """

    # TODO: Can be removed for silx 0.8
    @classproperty
    @deprecation.deprecated(replacement="DataViews.EMPTY_MODE", since_version="0.7", skip_backtrace_count=2)
    def EMPTY_MODE(self):
        return DataViews.EMPTY_MODE

    # TODO: Can be removed for silx 0.8
    @classproperty
    @deprecation.deprecated(replacement="DataViews.PLOT1D_MODE", since_version="0.7", skip_backtrace_count=2)
    def PLOT1D_MODE(self):
        return DataViews.PLOT1D_MODE

    # TODO: Can be removed for silx 0.8
    @classproperty
    @deprecation.deprecated(replacement="DataViews.PLOT2D_MODE", since_version="0.7", skip_backtrace_count=2)
    def PLOT2D_MODE(self):
        return DataViews.PLOT2D_MODE

    # TODO: Can be removed for silx 0.8
    @classproperty
    @deprecation.deprecated(replacement="DataViews.PLOT3D_MODE", since_version="0.7", skip_backtrace_count=2)
    def PLOT3D_MODE(self):
        return DataViews.PLOT3D_MODE

    # TODO: Can be removed for silx 0.8
    @classproperty
    @deprecation.deprecated(replacement="DataViews.RAW_MODE", since_version="0.7", skip_backtrace_count=2)
    def RAW_MODE(self):
        return DataViews.RAW_MODE

    # TODO: Can be removed for silx 0.8
    @classproperty
    @deprecation.deprecated(replacement="DataViews.RAW_ARRAY_MODE", since_version="0.7", skip_backtrace_count=2)
    def RAW_ARRAY_MODE(self):
        return DataViews.RAW_ARRAY_MODE

    # TODO: Can be removed for silx 0.8
    @classproperty
    @deprecation.deprecated(replacement="DataViews.RAW_RECORD_MODE", since_version="0.7", skip_backtrace_count=2)
    def RAW_RECORD_MODE(self):
        return DataViews.RAW_RECORD_MODE

    # TODO: Can be removed for silx 0.8
    @classproperty
    @deprecation.deprecated(replacement="DataViews.RAW_SCALAR_MODE", since_version="0.7", skip_backtrace_count=2)
    def RAW_SCALAR_MODE(self):
        return DataViews.RAW_SCALAR_MODE

    # TODO: Can be removed for silx 0.8
    @classproperty
    @deprecation.deprecated(replacement="DataViews.STACK_MODE", since_version="0.7", skip_backtrace_count=2)
    def STACK_MODE(self):
        return DataViews.STACK_MODE

    # TODO: Can be removed for silx 0.8
    @classproperty
    @deprecation.deprecated(replacement="DataViews.HDF5_MODE", since_version="0.7", skip_backtrace_count=2)
    def HDF5_MODE(self):
        return DataViews.HDF5_MODE

    displayedViewChanged = qt.Signal(object)
    """Emitted when the displayed view changes"""

    dataChanged = qt.Signal()
    """Emitted when the data changes"""

    currentAvailableViewsChanged = qt.Signal()
    """Emitted when the current available views (which support the current
    data) change"""

    def __init__(self, parent=None):
        """Constructor

        :param QWidget parent: The parent of the widget
        """
        super(DataViewer, self).__init__(parent)

        self.__stack = qt.QStackedWidget(self)
        self.__numpySelection = NumpyAxesSelector(self)
        self.__numpySelection.selectedAxisChanged.connect(self.__numpyAxisChanged)
        self.__numpySelection.selectionChanged.connect(self.__numpySelectionChanged)
        self.__numpySelection.customAxisChanged.connect(self.__numpyCustomAxisChanged)

        self.setLayout(qt.QVBoxLayout(self))
        self.layout().addWidget(self.__stack, 1)

        group = qt.QGroupBox(self)
        group.setLayout(qt.QVBoxLayout())
        group.layout().addWidget(self.__numpySelection)
        group.setTitle("Axis selection")
        self.__axisSelection = group

        self.layout().addWidget(self.__axisSelection)

        self.__currentAvailableViews = []
        self.__currentView = None
        self.__data = None
        self.__info = None
        self.__useAxisSelection = False
        self.__userSelectedView = None
        self.__hooks = None

        self.__views = []
        self.__index = {}
        """store stack index for each views"""

        self._initializeViews()

    def _initializeViews(self):
        """Inisialize the available views"""
        views = self.createDefaultViews(self.__stack)
        self.__views = list(views)
        self.setDisplayMode(DataViews.EMPTY_MODE)

    def setGlobalHooks(self, hooks):
        """Set a data view hooks for all the views

        :param DataViewHooks context: The hooks to use
        """
        self.__hooks = hooks
        for v in self.__views:
            v.setHooks(hooks)

    def createDefaultViews(self, parent=None):
        """Create and returns available views which can be displayed by default
        by the data viewer. It is called internally by the widget. It can be
        overwriten to provide a different set of viewers.

        :param QWidget parent: QWidget parent of the views
        :rtype: List[silx.gui.data.DataViews.DataView]
        """
        viewClasses = [
            DataViews._EmptyView,
            DataViews._Hdf5View,
            DataViews._NXdataView,
            DataViews._Plot1dView,
            DataViews._ImageView,
            DataViews._Plot3dView,
            DataViews._RawView,
            DataViews._StackView,
        ]
        views = []
        for viewClass in viewClasses:
            try:
                view = viewClass(parent)
                views.append(view)
            except Exception:
                _logger.warning("%s instantiation failed. View is ignored" % viewClass.__name__)
                _logger.debug("Backtrace", exc_info=True)

        return views

    def clear(self):
        """Clear the widget"""
        self.setData(None)

    def normalizeData(self, data):
        """Returns a normalized data if the embed a numpy or a dataset.
        Else returns the data."""
        return _normalizeData(data)

    def __getStackIndex(self, view):
        """Get the stack index containing the view.

        :param silx.gui.data.DataViews.DataView view: The view
        """
        if view not in self.__index:
            widget = view.getWidget()
            index = self.__stack.addWidget(widget)
            self.__index[view] = index
        else:
            index = self.__index[view]
        return index

    def __clearCurrentView(self):
        """Clear the current selected view"""
        view = self.__currentView
        if view is not None:
            view.clear()

    def __numpyCustomAxisChanged(self, name, value):
        view = self.__currentView
        if view is not None:
            view.setCustomAxisValue(name, value)

    def __updateNumpySelectionAxis(self):
        """
        Update the numpy-selector according to the needed axis names
        """
        previous = self.__numpySelection.blockSignals(True)
        self.__numpySelection.clear()
        info = self._getInfo()
        axisNames = self.__currentView.axesNames(self.__data, info)
        if info.isArray and info.size != 0 and self.__data is not None and axisNames is not None:
            self.__useAxisSelection = True
            self.__numpySelection.setAxisNames(axisNames)
            self.__numpySelection.setCustomAxis(self.__currentView.customAxisNames())
            data = self.normalizeData(self.__data)
            self.__numpySelection.setData(data)
            if hasattr(data, "shape"):
                isVisible = not (len(axisNames) == 1 and len(data.shape) == 1)
            else:
                isVisible = True
            self.__axisSelection.setVisible(isVisible)
        else:
            self.__useAxisSelection = False
            self.__axisSelection.setVisible(False)
        self.__numpySelection.blockSignals(previous)

    def __updateDataInView(self):
        """
        Update the views using the current data
        """
        if self.__useAxisSelection:
            self.__displayedData = self.__numpySelection.selectedData()
        else:
            self.__displayedData = self.__data

        qt.QTimer.singleShot(10, self.__setDataInView)

    def __setDataInView(self):
        self.__currentView.setData(self.__displayedData)

    def setDisplayedView(self, view):
        """Set the displayed view.

        Change the displayed view according to the view itself.

        :param silx.gui.data.DataViews.DataView view: The DataView to use to display the data
        """
        self.__userSelectedView = view
        self._setDisplayedView(view)

    def _setDisplayedView(self, view):
        """Internal set of the displayed view.

        Change the displayed view according to the view itself.

        :param silx.gui.data.DataViews.DataView view: The DataView to use to display the data
        """
        if self.__currentView is view:
            return
        self.__clearCurrentView()
        self.__currentView = view
        self.__updateNumpySelectionAxis()
        self.__updateDataInView()
        stackIndex = self.__getStackIndex(self.__currentView)
        if self.__currentView is not None:
            self.__currentView.select()
        self.__stack.setCurrentIndex(stackIndex)
        self.displayedViewChanged.emit(view)

    def getViewFromModeId(self, modeId):
        """Returns the first available view which have the requested modeId.
        Return None if modeId does not correspond to an existing view.

        :param int modeId: Requested mode id
        :rtype: silx.gui.data.DataViews.DataView
        """
        for view in self.__views:
            if view.modeId() == modeId:
                return view
        return None

    def setDisplayMode(self, modeId):
        """Set the displayed view using display mode.

        Change the displayed view according to the requested mode.

        :param int modeId: Display mode, one of

            - `DataViews.EMPTY_MODE`: display nothing
            - `DataViews.PLOT1D_MODE`: display the data as a curve
            - `DataViews.IMAGE_MODE`: display the data as an image
            - `DataViews.PLOT3D_MODE`: display the data as an isosurface
            - `DataViews.RAW_MODE`: display the data as a table
            - `DataViews.STACK_MODE`: display the data as a stack of images
            - `DataViews.HDF5_MODE`: display the data as a table of HDF5 info
            - `DataViews.NXDATA_MODE`: display the data as NXdata
        """
        try:
            view = self.getViewFromModeId(modeId)
        except KeyError:
            raise ValueError("Display mode %s is unknown" % modeId)
        self._setDisplayedView(view)

    def displayedView(self):
        """Returns the current displayed view.

        :rtype: silx.gui.data.DataViews.DataView
        """
        return self.__currentView

    def addView(self, view):
        """Allow to add a view to the dataview.

        If the current data support this view, it will be displayed.

        :param DataView view: A dataview
        """
        if self.__hooks is not None:
            view.setHooks(self.__hooks)
        self.__views.append(view)
        # TODO It can be skipped if the view do not support the data
        self.__updateAvailableViews()

    def removeView(self, view):
        """Allow to remove a view which was available from the dataview.

        If the view was displayed, the widget will be updated.

        :param DataView view: A dataview
        """
        self.__views.remove(view)
        self.__stack.removeWidget(view.getWidget())
        # invalidate the full index. It will be updated as expected
        self.__index = {}

        if self.__userSelectedView is view:
            self.__userSelectedView = None

        if view is self.__currentView:
            self.__updateView()
        else:
            # TODO It can be skipped if the view is not part of the
            # available views
            self.__updateAvailableViews()

    def __updateAvailableViews(self):
        """
        Update available views from the current data.
        """
        data = self.__data
        info = self._getInfo()
        # sort available views according to priority
        priorities = [v.getDataPriority(data, info) for v in self.__views]
        views = zip(priorities, self.__views)
        views = filter(lambda t: t[0] > DataViews.DataView.UNSUPPORTED, views)
        views = sorted(views, reverse=True)

        # store available views
        if len(views) == 0:
            self.__setCurrentAvailableViews([])
            available = []
        else:
            available = [v[1] for v in views]
            self.__setCurrentAvailableViews(available)

    def __updateView(self):
        """Display the data using the widget which fit the best"""
        data = self.__data

        # update available views for this data
        self.__updateAvailableViews()
        available = self.__currentAvailableViews

        # display the view with the most priority (the default view)
        view = self.getDefaultViewFromAvailableViews(data, available)
        self.__clearCurrentView()
        try:
            self._setDisplayedView(view)
        except Exception as e:
            # in case there is a problem to read the data, try to use a safe
            # view
            view = self.getSafeViewFromAvailableViews(data, available)
            self._setDisplayedView(view)
            raise e

    def getSafeViewFromAvailableViews(self, data, available):
        """Returns a view which is sure to display something without failing
        on rendering.

        :param object data: data which will be displayed
        :param List[view] available: List of available views, from highest
            priority to lowest.
        :rtype: DataView
        """
        hdf5View = self.getViewFromModeId(DataViewer.HDF5_MODE)
        if hdf5View in available:
            return hdf5View
        return self.getViewFromModeId(DataViews.EMPTY_MODE)

    def getDefaultViewFromAvailableViews(self, data, available):
        """Returns the default view which will be used according to available
        views.

        :param object data: data which will be displayed
        :param List[view] available: List of available views, from highest
            priority to lowest.
        :rtype: DataView
        """
        if len(available) > 0:
            # returns the view with the highest priority
            if self.__userSelectedView in available:
                return self.__userSelectedView
            self.__userSelectedView = None
            view = available[0]
        else:
            # else returns the empty view
            view = self.getViewFromModeId(DataViews.EMPTY_MODE)
        return view

    def __setCurrentAvailableViews(self, availableViews):
        """Set the current available viewa

        :param List[DataView] availableViews: Current available viewa
        """
        self.__currentAvailableViews = availableViews
        self.currentAvailableViewsChanged.emit()

    def currentAvailableViews(self):
        """Returns the list of available views for the current data

        :rtype: List[DataView]
        """
        return self.__currentAvailableViews

    def availableViews(self):
        """Returns the list of registered views

        :rtype: List[DataView]
        """
        return self.__views

    def setData(self, data):
        """Set the data to view.

        It mostly can be a h5py.Dataset or a numpy.ndarray. Other kind of
        objects will be displayed as text rendering.

        :param numpy.ndarray data: The data.
        """
        self.__data = data
        self._invalidateInfo()
        self.__displayedData = None
        self.__updateView()
        self.__updateNumpySelectionAxis()
        self.__updateDataInView()
        self.dataChanged.emit()

    def __numpyAxisChanged(self):
        """
        Called when axis selection of the numpy-selector changed
        """
        self.__clearCurrentView()

    def __numpySelectionChanged(self):
        """
        Called when data selection of the numpy-selector changed
        """
        self.__updateDataInView()

    def data(self):
        """Returns the data"""
        return self.__data

    def _invalidateInfo(self):
        """Invalidate DataInfo cache."""
        self.__info = None

    def _getInfo(self):
        """Returns the DataInfo of the current selected data.

        This value is cached.

        :rtype: DataInfo
        """
        if self.__info is None:
            self.__info = DataViews.DataInfo(self.__data)
        return self.__info

    def displayMode(self):
        """Returns the current display mode"""
        return self.__currentView.modeId()

    def replaceView(self, modeId, newView):
        """Replace one of the builtin data views with a custom view.
        Return True in case of success, False in case of failure.

        .. note::

            This method must be called just after instantiation, before
            the viewer is used.

        :param int modeId: Unique mode ID identifying the DataView to
            be replaced. One of:

            - `DataViews.EMPTY_MODE`
            - `DataViews.PLOT1D_MODE`
            - `DataViews.IMAGE_MODE`
            - `DataViews.PLOT2D_MODE`
            - `DataViews.COMPLEX_IMAGE_MODE`
            - `DataViews.PLOT3D_MODE`
            - `DataViews.RAW_MODE`
            - `DataViews.STACK_MODE`
            - `DataViews.HDF5_MODE`
            - `DataViews.NXDATA_MODE`
            - `DataViews.NXDATA_INVALID_MODE`
            - `DataViews.NXDATA_SCALAR_MODE`
            - `DataViews.NXDATA_CURVE_MODE`
            - `DataViews.NXDATA_XYVSCATTER_MODE`
            - `DataViews.NXDATA_IMAGE_MODE`
            - `DataViews.NXDATA_STACK_MODE`

        :param DataViews.DataView newView: New data view
        :return: True if replacement was successful, else False
        """
        assert isinstance(newView, DataViews.DataView)
        isReplaced = False
        for idx, view in enumerate(self.__views):
            if view.modeId() == modeId:
                if self.__hooks is not None:
                    newView.setHooks(self.__hooks)
                self.__views[idx] = newView
                isReplaced = True
                break
            elif isinstance(view, DataViews.CompositeDataView):
                isReplaced = view.replaceView(modeId, newView)
                if isReplaced:
                    break

        if isReplaced:
            self.__updateAvailableViews()
        return isReplaced