diff options
Diffstat (limited to 'silx/gui/plot/StatsWidget.py')
-rw-r--r-- | silx/gui/plot/StatsWidget.py | 1594 |
1 files changed, 1154 insertions, 440 deletions
diff --git a/silx/gui/plot/StatsWidget.py b/silx/gui/plot/StatsWidget.py index bb66613..4ba4fab 100644 --- a/silx/gui/plot/StatsWidget.py +++ b/silx/gui/plot/StatsWidget.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017-2018 European Synchrotron Radiation Facility +# Copyright (c) 2017-2019 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 @@ -31,552 +31,1266 @@ __license__ = "MIT" __date__ = "24/07/2018" -import functools +from collections import OrderedDict +from contextlib import contextmanager import logging +import weakref + import numpy -from collections import OrderedDict -import silx.utils.weakref from silx.gui import qt from silx.gui import icons -from silx.gui.plot.items.curve import Curve as CurveItem -from silx.gui.plot.items.histogram import Histogram as HistogramItem -from silx.gui.plot.items.image import ImageBase as ImageItem -from silx.gui.plot.items.scatter import Scatter as ScatterItem from silx.gui.plot import stats as statsmdl from silx.gui.widgets.TableWidget import TableWidget from silx.gui.plot.stats.statshandler import StatsHandler, StatFormatter +from silx.gui.plot.items.core import ItemChangedType +from silx.gui.widgets.FlowLayout import FlowLayout +from . import PlotWidget +from . import items as plotitems -logger = logging.getLogger(__name__) +_logger = logging.getLogger(__name__) -class StatsWidget(qt.QWidget): + +# Helper class to handle specific calls to PlotWidget and SceneWidget + +class _Wrapper(qt.QObject): + """Base class for connection with PlotWidget and SceneWidget. + + This class is used when no PlotWidget or SceneWidget is connected. + + :param plot: The plot to be used """ - Widget displaying a set of :class:`Stat` to be displayed on a - :class:`StatsTable` and to be apply on items contained in the :class:`Plot` - Also contains options to: - * compute statistics on all the data or on visible data only - * show statistics of all items or only the active one + sigItemAdded = qt.Signal(object) + """Signal emitted when a new item is added. - :param parent: Qt parent - :param plot: the plot containing items on which we want statistics. + It provides the added item. """ - sigVisibilityChanged = qt.Signal(bool) + sigItemRemoved = qt.Signal(object) + """Signal emitted when an item is (about to be) removed. - NUMBER_FORMAT = '{0:.3f}' + It provides the removed item. + """ - class OptionsWidget(qt.QToolBar): - - def __init__(self, parent=None): - qt.QToolBar.__init__(self, parent) - self.setIconSize(qt.QSize(16, 16)) - - action = qt.QAction(self) - action.setIcon(icons.getQIcon("stats-active-items")) - action.setText("Active items only") - action.setToolTip("Display stats for active items only.") - action.setCheckable(True) - action.setChecked(True) - self.__displayActiveItems = action - - action = qt.QAction(self) - action.setIcon(icons.getQIcon("stats-whole-items")) - action.setText("All items") - action.setToolTip("Display stats for all available items.") - action.setCheckable(True) - self.__displayWholeItems = action - - action = qt.QAction(self) - action.setIcon(icons.getQIcon("stats-visible-data")) - action.setText("Use the visible data range") - action.setToolTip("Use the visible data range.<br/>" - "If activated the data is filtered to only use" - "visible data of the plot." - "The filtering is a data sub-sampling." - "No interpolation is made to fit data to" - "boundaries.") - action.setCheckable(True) - self.__useVisibleData = action - - action = qt.QAction(self) - action.setIcon(icons.getQIcon("stats-whole-data")) - action.setText("Use the full data range") - action.setToolTip("Use the full data range.") - action.setCheckable(True) - action.setChecked(True) - self.__useWholeData = action - - self.addAction(self.__displayWholeItems) - self.addAction(self.__displayActiveItems) - self.addSeparator() - self.addAction(self.__useVisibleData) - self.addAction(self.__useWholeData) - - self.itemSelection = qt.QActionGroup(self) - self.itemSelection.setExclusive(True) - self.itemSelection.addAction(self.__displayActiveItems) - self.itemSelection.addAction(self.__displayWholeItems) - - self.dataRangeSelection = qt.QActionGroup(self) - self.dataRangeSelection.setExclusive(True) - self.dataRangeSelection.addAction(self.__useWholeData) - self.dataRangeSelection.addAction(self.__useVisibleData) - - def isActiveItemMode(self): - return self.itemSelection.checkedAction() is self.__displayActiveItems - - def isVisibleDataRangeMode(self): - return self.dataRangeSelection.checkedAction() is self.__useVisibleData + sigCurrentChanged = qt.Signal(object) + """Signal emitted when the current item has changed. - def __init__(self, parent=None, plot=None, stats=None): - qt.QWidget.__init__(self, parent) - self.setLayout(qt.QVBoxLayout()) - self.layout().setContentsMargins(0, 0, 0, 0) - self._options = self.OptionsWidget(parent=self) - self.layout().addWidget(self._options) - self._statsTable = StatsTable(parent=self, plot=plot) - self.setStats = self._statsTable.setStats - self.setStats(stats) + It provides the current item. + """ - self.layout().addWidget(self._statsTable) - self.setPlot = self._statsTable.setPlot + sigVisibleDataChanged = qt.Signal() + """Signal emitted when the visible data area has changed""" - self._options.itemSelection.triggered.connect( - self._optSelectionChanged) - self._options.dataRangeSelection.triggered.connect( - self._optDataRangeChanged) - self._optSelectionChanged() - self._optDataRangeChanged() + def __init__(self, plot=None): + super(_Wrapper, self).__init__(parent=None) + self._plotRef = None if plot is None else weakref.ref(plot) - self.setDisplayOnlyActiveItem = self._statsTable.setDisplayOnlyActiveItem - self.setStatsOnVisibleData = self._statsTable.setStatsOnVisibleData + def getPlot(self): + """Returns the plot attached to this widget""" + return None if self._plotRef is None else self._plotRef() - def showEvent(self, event): - self.sigVisibilityChanged.emit(True) - qt.QWidget.showEvent(self, event) + def getItems(self): + """Returns the list of items in the plot - def hideEvent(self, event): - self.sigVisibilityChanged.emit(False) - qt.QWidget.hideEvent(self, event) + :rtype: List[object] + """ + return () - def _optSelectionChanged(self, action=None): - self._statsTable.setDisplayOnlyActiveItem(self._options.isActiveItemMode()) + def getSelectedItems(self): + """Returns the list of selected items in the plot - def _optDataRangeChanged(self, action=None): - self._statsTable.setStatsOnVisibleData(self._options.isVisibleDataRangeMode()) + :rtype: List[object] + """ + return () + def setCurrentItem(self, item): + """Set the current/active item in the plot -class BasicStatsWidget(StatsWidget): + :param item: The plot item to set as active/current + """ + pass + + def getLabel(self, item): + """Returns the label of the given item. + + :param item: + :rtype: str + """ + return '' + + def getKind(self, item): + """Returns the kind of an item or None if not supported + + :param item: + :rtype: Union[str,None] + """ + return None + + +class _PlotWidgetWrapper(_Wrapper): + """Class handling PlotWidget specific calls and signal connections + + See :class:`._Wrapper` for documentation + + :param PlotWidget plot: """ - Widget defining a simple set of :class:`Stat` to be displayed on a - :class:`StatsWidget`. - :param parent: Qt parent - :param plot: the plot containing items on which we want statistics. + def __init__(self, plot): + assert isinstance(plot, PlotWidget) + super(_PlotWidgetWrapper, self).__init__(plot) + plot.sigItemAdded.connect(self.sigItemAdded.emit) + plot.sigItemAboutToBeRemoved.connect(self.sigItemRemoved.emit) + plot.sigActiveCurveChanged.connect(self._activeCurveChanged) + plot.sigActiveImageChanged.connect(self._activeImageChanged) + plot.sigActiveScatterChanged.connect(self._activeScatterChanged) + plot.sigPlotSignal.connect(self._limitsChanged) + + def _activeChanged(self, kind): + """Handle change of active curve/image/scatter""" + plot = self.getPlot() + if plot is not None: + item = plot._getActiveItem(kind=kind) + if item is None or self.getKind(item) is not None: + self.sigCurrentChanged.emit(item) + + def _activeCurveChanged(self, previous, current): + self._activeChanged(kind='curve') + + def _activeImageChanged(self, previous, current): + self._activeChanged(kind='image') + + def _activeScatterChanged(self, previous, current): + self._activeChanged(kind='scatter') + + def _limitsChanged(self, event): + """Handle change of plot area limits.""" + if event['event'] == 'limitsChanged': + self.sigVisibleDataChanged.emit() + + def getItems(self): + plot = self.getPlot() + return () if plot is None else plot._getItems() + + def getSelectedItems(self): + plot = self.getPlot() + items = [] + if plot is not None: + for kind in plot._ACTIVE_ITEM_KINDS: + item = plot._getActiveItem(kind=kind) + if item is not None: + items.append(item) + return tuple(items) + + def setCurrentItem(self, item): + plot = self.getPlot() + if plot is not None: + kind = self.getKind(item) + if kind in plot._ACTIVE_ITEM_KINDS: + if plot._getActiveItem(kind) != item: + plot._setActiveItem(kind, item.getLegend()) + + def getLabel(self, item): + return item.getLegend() + + def getKind(self, item): + if isinstance(item, plotitems.Curve): + return 'curve' + elif isinstance(item, plotitems.ImageData): + return 'image' + elif isinstance(item, plotitems.Scatter): + return 'scatter' + elif isinstance(item, plotitems.Histogram): + return 'histogram' + else: + return None + + +class _SceneWidgetWrapper(_Wrapper): + """Class handling SceneWidget specific calls and signal connections + + See :class:`._Wrapper` for documentation + + :param SceneWidget plot: """ - STATS = StatsHandler(( - (statsmdl.StatMin(), StatFormatter()), - statsmdl.StatCoordMin(), - (statsmdl.StatMax(), StatFormatter()), - statsmdl.StatCoordMax(), - (('std', numpy.std), StatFormatter()), - (('mean', numpy.mean), StatFormatter()), - statsmdl.StatCOM() - )) + def __init__(self, plot): + # Lazy-import to avoid circular imports + from ..plot3d.SceneWidget import SceneWidget - def __init__(self, parent=None, plot=None): - StatsWidget.__init__(self, parent=parent, plot=plot, stats=self.STATS) + assert isinstance(plot, SceneWidget) + super(_SceneWidgetWrapper, self).__init__(plot) + plot.getSceneGroup().sigItemAdded.connect(self.sigItemAdded) + plot.getSceneGroup().sigItemRemoved.connect(self.sigItemRemoved) + plot.selection().sigCurrentChanged.connect(self._currentChanged) + # sigVisibleDataChanged is never emitted + + def _currentChanged(self, current, previous): + self.sigCurrentChanged.emit(current) + + def getItems(self): + plot = self.getPlot() + return () if plot is None else tuple(plot.getSceneGroup().visit()) + + def getSelectedItems(self): + plot = self.getPlot() + return () if plot is None else (plot.selection().getCurrentItem(),) + def setCurrentItem(self, item): + plot = self.getPlot() + if plot is not None: + plot.selection().setCurrentItem(item) -class StatsTable(TableWidget): + def getLabel(self, item): + return item.getLabel() + + def getKind(self, item): + from ..plot3d import items as plot3ditems + + if isinstance(item, (plot3ditems.ImageData, + plot3ditems.ScalarField3D)): + return 'image' + elif isinstance(item, (plot3ditems.Scatter2D, + plot3ditems.Scatter3D)): + return 'scatter' + else: + return None + + +class _ScalarFieldViewWrapper(_Wrapper): + """Class handling ScalarFieldView specific calls and signal connections + + See :class:`._Wrapper` for documentation + + :param SceneWidget plot: """ - TableWidget displaying for each curves contained by the Plot some - information: - * legend - * minimal value - * maximal value - * standard deviation (std) + def __init__(self, plot): + # Lazy-import to avoid circular imports + from ..plot3d.ScalarFieldView import ScalarFieldView + from ..plot3d.items import ScalarField3D + + assert isinstance(plot, ScalarFieldView) + super(_ScalarFieldViewWrapper, self).__init__(plot) + self._item = ScalarField3D() + self._dataChanged() + plot.sigDataChanged.connect(self._dataChanged) + # sigItemAdded, sigItemRemoved, sigVisibleDataChanged are never emitted + + def _dataChanged(self): + plot = self.getPlot() + if plot is not None: + self._item.setData(plot.getData(copy=False), copy=False) + self.sigCurrentChanged.emit(self._item) - :param parent: The widget's parent. - :param plot: :class:`.PlotWidget` instance on which to operate + def getItems(self): + plot = self.getPlot() + return () if plot is None else (self._item,) + + def getSelectedItems(self): + return self.getItems() + + def setCurrentItem(self, item): + pass + + def getLabel(self, item): + return 'Data' + + def getKind(self, item): + return 'image' + + +class _Container(object): + """Class to contain a plot item. + + This is apparently needed for compatibility with PySide2, + + :param QObject obj: """ + def __init__(self, obj): + self._obj = obj - COMPATIBLE_KINDS = { - 'curve': CurveItem, - 'image': ImageItem, - 'scatter': ScatterItem, - 'histogram': HistogramItem - } + def __call__(self): + return self._obj - COMPATIBLE_ITEMS = tuple(COMPATIBLE_KINDS.values()) - def __init__(self, parent=None, plot=None): - TableWidget.__init__(self, parent) - """Next freeID for the curve""" - self.plot = None - self._displayOnlyActItem = False - self._statsOnVisibleData = False - self._lgdAndKindToItems = {} - """Associate to a tuple(legend, kind) the items legend""" - self.callbackImage = None - self.callbackScatter = None - self.callbackCurve = None - """Associate the curve legend to his first item""" +class _StatsWidgetBase(object): + """ + Base class for all widgets which want to display statistics + """ + def __init__(self, statsOnVisibleData, displayOnlyActItem): + self._displayOnlyActItem = displayOnlyActItem + self._statsOnVisibleData = statsOnVisibleData self._statsHandler = None - self._legendsSet = [] - """list of legends actually displayed""" - self._resetColumns() - self.setColumnCount(len(self._columns)) - self.setSelectionBehavior(qt.QAbstractItemView.SelectRows) - self.setPlot(plot) - self.setSortingEnabled(True) + self.__default_skipped_events = ( + ItemChangedType.ALPHA, + ItemChangedType.COLOR, + ItemChangedType.COLORMAP, + ItemChangedType.SYMBOL, + ItemChangedType.SYMBOL_SIZE, + ItemChangedType.LINE_WIDTH, + ItemChangedType.LINE_STYLE, + ItemChangedType.LINE_BG_COLOR, + ItemChangedType.FILL, + ItemChangedType.HIGHLIGHTED_COLOR, + ItemChangedType.HIGHLIGHTED_STYLE, + ItemChangedType.TEXT, + ItemChangedType.OVERLAY, + ItemChangedType.VISUALIZATION_MODE, + ) + + self._plotWrapper = _Wrapper() + self._dealWithPlotConnection(create=True) - def _resetColumns(self): - self._columns_index = OrderedDict([('legend', 0), ('kind', 1)]) - self._columns = self._columns_index.keys() - self.setColumnCount(len(self._columns)) + def setPlot(self, plot): + """Define the plot to interact with - def setStats(self, statsHandler): + :param Union[PlotWidget,SceneWidget,None] plot: + The plot containing the items on which statistics are applied """ + try: + import OpenGL + except ImportError: + has_opengl = False + else: + has_opengl = True + from ..plot3d.SceneWidget import SceneWidget # Lazy import + self._dealWithPlotConnection(create=False) + self.clear() + if plot is None: + self._plotWrapper = _Wrapper() + elif isinstance(plot, PlotWidget): + self._plotWrapper = _PlotWidgetWrapper(plot) + else: + if has_opengl is True: + if isinstance(plot, SceneWidget): + self._plotWrapper = _SceneWidgetWrapper(plot) + else: # Expect a ScalarFieldView + self._plotWrapper = _ScalarFieldViewWrapper(plot) + else: + _logger.warning('OpenGL not installed, %s not managed' % ('SceneWidget qnd ScalarFieldView')) + self._dealWithPlotConnection(create=True) + + def setStats(self, statsHandler): + """Set which stats to display and the associated formatting. - :param statsHandler: Set the statistics to be displayed and how to - format them using - :rtype: :class:`StatsHandler` + :param StatsHandler statsHandler: + Set the statistics to be displayed and how to format them using """ - _statsHandler = statsHandler if statsHandler is None: - _statsHandler = StatsHandler(statFormatters=()) - if isinstance(_statsHandler, (list, tuple)): - _statsHandler = StatsHandler(_statsHandler) - assert isinstance(_statsHandler, StatsHandler) - self._resetColumns() - self.clear() - - for statName, stat in list(_statsHandler.stats.items()): - assert isinstance(stat, statsmdl.StatBase) - self._columns_index[statName] = len(self._columns_index) - self._statsHandler = _statsHandler - self._columns = self._columns_index.keys() - self.setColumnCount(len(self._columns)) + statsHandler = StatsHandler(statFormatters=()) + elif isinstance(statsHandler, (list, tuple)): + statsHandler = StatsHandler(statsHandler) + assert isinstance(statsHandler, StatsHandler) - self._updateItemObserve() - self._updateAllStats() + self._statsHandler = statsHandler def getStatsHandler(self): + """Returns the :class:`StatsHandler` in use. + + :rtype: StatsHandler + """ return self._statsHandler - def _updateAllStats(self): - for (legend, kind) in self._lgdAndKindToItems: - self._updateStats(legend, kind) + def getPlot(self): + """Returns the plot attached to this widget - @staticmethod - def _getKind(myItem): - if isinstance(myItem, CurveItem): - return 'curve' - elif isinstance(myItem, ImageItem): - return 'image' - elif isinstance(myItem, ScatterItem): - return 'scatter' - elif isinstance(myItem, HistogramItem): - return 'histogram' + :rtype: Union[PlotWidget,SceneWidget,None] + """ + return self._plotWrapper.getPlot() + + def _dealWithPlotConnection(self, create=True): + """Manage connection to plot signals + + Note: connection on Item are managed by _addItem and _removeItem methods + """ + connections = [] # List of (signal, slot) to connect/disconnect + if self._statsOnVisibleData: + connections.append( + (self._plotWrapper.sigVisibleDataChanged, self._updateAllStats)) + + if self._displayOnlyActItem: + connections.append( + (self._plotWrapper.sigCurrentChanged, self._updateItemObserve)) else: - return None + connections += [ + (self._plotWrapper.sigItemAdded, self._addItem), + (self._plotWrapper.sigItemRemoved, self._removeItem), + (self._plotWrapper.sigCurrentChanged, self._plotCurrentChanged)] + + for signal, slot in connections: + if create: + signal.connect(slot) + else: + signal.disconnect(slot) - def setPlot(self, plot): + def _updateItemObserve(self, *args): + """Reload table depending on mode""" + raise NotImplementedError('Base class') + + def _updateStats(self, item): + """Update displayed information for given plot item + + :param item: The plot item + """ + raise NotImplementedError('Base class') + + def _updateAllStats(self): + """Update stats for all rows in the table""" + raise NotImplementedError('Base class') + + def setDisplayOnlyActiveItem(self, displayOnlyActItem): + """Toggle display off all items or only the active/selected one + + :param bool displayOnlyActItem: + True if we want to only show active item """ - Define the plot to interact with + self._displayOnlyActItem = displayOnlyActItem + + def setStatsOnVisibleData(self, b): + """Toggle computation of statistics on whole data or only visible ones. + + .. warning:: When visible data is activated we will process to a simple + filtering of visible data by the user. The filtering is a + simple data sub-sampling. No interpolation is made to fit + data to boundaries. - :param plot: the plot containing the items on which statistics are - applied - :rtype: :class:`.PlotWidget` + :param bool b: True if we want to apply statistics only on visible data """ - if self.plot: + if self._statsOnVisibleData != b: self._dealWithPlotConnection(create=False) - self.plot = plot - self.clear() - if self.plot: + self._statsOnVisibleData = b self._dealWithPlotConnection(create=True) - self._updateItemObserve() + self._updateAllStats() - def _updateItemObserve(self): - if self.plot: - self.clear() - if self._displayOnlyActItem is True: - activeCurve = self.plot.getActiveCurve(just_legend=False) - activeScatter = self.plot._getActiveItem(kind='scatter', - just_legend=False) - activeImage = self.plot.getActiveImage(just_legend=False) - if activeCurve: - self._addItem(activeCurve) - if activeImage: - self._addItem(activeImage) - if activeScatter: - self._addItem(activeScatter) - else: - [self._addItem(curve) for curve in self.plot.getAllCurves()] - [self._addItem(image) for image in self.plot.getAllImages()] - scatters = self.plot._getItems(kind='scatter', - just_legend=False, - withhidden=True) - [self._addItem(scatter) for scatter in scatters] - histograms = self.plot._getItems(kind='histogram', - just_legend=False, - withhidden=True) - [self._addItem(histogram) for histogram in histograms] + def _addItem(self, item): + """Add a plot item to the table - def _dealWithPlotConnection(self, create=True): + If item is not supported, it is ignored. + + :param item: The plot item + :returns: True if the item is added to the widget. + :rtype: bool """ - Manage connection to plot signals + raise NotImplementedError('Base class') - Note: connection on Item are managed by the _removeItem function + def _removeItem(self, item): + """Remove table items corresponding to given plot item from the table. + + :param item: The plot item """ - if self.plot is None: - return - if self._displayOnlyActItem: - if create is True: - if self.callbackImage is None: - self.callbackImage = functools.partial(self._activeItemChanged, 'image') - self.callbackScatter = functools.partial(self._activeItemChanged, 'scatter') - self.callbackCurve = functools.partial(self._activeItemChanged, 'curve') - self.plot.sigActiveImageChanged.connect(self.callbackImage) - self.plot.sigActiveScatterChanged.connect(self.callbackScatter) - self.plot.sigActiveCurveChanged.connect(self.callbackCurve) - else: - if self.callbackImage is not None: - self.plot.sigActiveImageChanged.disconnect(self.callbackImage) - self.plot.sigActiveScatterChanged.disconnect(self.callbackScatter) - self.plot.sigActiveCurveChanged.disconnect(self.callbackCurve) - self.callbackImage = None - self.callbackScatter = None - self.callbackCurve = None - else: - if create is True: - self.plot.sigContentChanged.connect(self._plotContentChanged) - else: - self.plot.sigContentChanged.disconnect(self._plotContentChanged) - if create is True: - self.plot.sigPlotSignal.connect(self._zoomPlotChanged) - else: - self.plot.sigPlotSignal.disconnect(self._zoomPlotChanged) + raise NotImplementedError('Base class') + + def _plotCurrentChanged(self, current): + """Handle change of current item and update selection in table + + :param current: + """ + raise NotImplementedError('Base class') def clear(self): + """clear GUI""" + pass + + def _skipPlotItemChangedEvent(self, event): """ - Clear all existing items + + :param ItemChangedtype event: event to filter or not + :return: True if we want to ignore this ItemChangedtype + :rtype: bool """ - lgdsAndKinds = list(self._lgdAndKindToItems.keys()) - for lgdAndKind in lgdsAndKinds: - self._removeItem(legend=lgdAndKind[0], kind=lgdAndKind[1]) - self._lgdAndKindToItems = {} - qt.QTableWidget.clear(self) + return event in self.__default_skipped_events + + +class StatsTable(_StatsWidgetBase, TableWidget): + """ + TableWidget displaying for each curves contained by the Plot some + information: + + * legend + * minimal value + * maximal value + * standard deviation (std) + + :param QWidget parent: The widget's parent. + :param Union[PlotWidget,SceneWidget] plot: + :class:`PlotWidget` or :class:`SceneWidget` instance on which to operate + """ + + _LEGEND_HEADER_DATA = 'legend' + _KIND_HEADER_DATA = 'kind' + + def __init__(self, parent=None, plot=None): + TableWidget.__init__(self, parent) + _StatsWidgetBase.__init__(self, statsOnVisibleData=False, + displayOnlyActItem=False) + + # Init for _displayOnlyActItem == False + assert self._displayOnlyActItem is False + self.setSelectionBehavior(qt.QAbstractItemView.SelectRows) + self.setSelectionMode(qt.QAbstractItemView.SingleSelection) + self.currentItemChanged.connect(self._currentItemChanged) + self.setRowCount(0) + self.setColumnCount(2) - # It have to called befor3e accessing to the header items - self.setHorizontalHeaderLabels(list(self._columns)) - - if self._statsHandler is not None: - for columnId, name in enumerate(self._columns): - item = self.horizontalHeaderItem(columnId) - if name in self._statsHandler.stats: - stat = self._statsHandler.stats[name] - text = stat.name[0].upper() + stat.name[1:] - if stat.description is not None: - tooltip = stat.description - else: - tooltip = "" - else: - text = name[0].upper() + name[1:] - tooltip = "" - item.setToolTip(tooltip) - item.setText(text) + # Init headers + headerItem = qt.QTableWidgetItem('Legend') + headerItem.setData(qt.Qt.UserRole, self._LEGEND_HEADER_DATA) + self.setHorizontalHeaderItem(0, headerItem) + headerItem = qt.QTableWidgetItem('Kind') + headerItem.setData(qt.Qt.UserRole, self._KIND_HEADER_DATA) + self.setHorizontalHeaderItem(1, headerItem) + + self.setSortingEnabled(True) + self.setPlot(plot) - if hasattr(self.horizontalHeader(), 'setSectionResizeMode'): # Qt5 - self.horizontalHeader().setSectionResizeMode(qt.QHeaderView.ResizeToContents) + @contextmanager + def _disableSorting(self): + """Context manager that disables table sorting + + Previous state is restored when leaving + """ + sorting = self.isSortingEnabled() + if sorting: + self.setSortingEnabled(False) + yield + if sorting: + self.setSortingEnabled(sorting) + + def setStats(self, statsHandler): + """Set which stats to display and the associated formatting. + + :param StatsHandler statsHandler: + Set the statistics to be displayed and how to format them using + """ + self._removeAllItems() + _StatsWidgetBase.setStats(self, statsHandler) + + self.setRowCount(0) + self.setColumnCount(len(self._statsHandler.stats) + 2) # + legend and kind + + for index, stat in enumerate(self._statsHandler.stats.values()): + headerItem = qt.QTableWidgetItem(stat.name.capitalize()) + headerItem.setData(qt.Qt.UserRole, stat.name) + if stat.description is not None: + headerItem.setToolTip(stat.description) + self.setHorizontalHeaderItem(2 + index, headerItem) + + horizontalHeader = self.horizontalHeader() + if hasattr(horizontalHeader, 'setSectionResizeMode'): # Qt5 + horizontalHeader.setSectionResizeMode(qt.QHeaderView.ResizeToContents) else: # Qt4 - self.horizontalHeader().setResizeMode(qt.QHeaderView.ResizeToContents) - self.setColumnHidden(self._columns_index['kind'], True) + horizontalHeader.setResizeMode(qt.QHeaderView.ResizeToContents) - def _addItem(self, item): - assert isinstance(item, self.COMPATIBLE_ITEMS) - if (item.getLegend(), self._getKind(item)) in self._lgdAndKindToItems: - self._updateStats(item.getLegend(), self._getKind(item)) - return + self._updateItemObserve() + + def setPlot(self, plot): + """Define the plot to interact with + + :param Union[PlotWidget,SceneWidget,None] plot: + The plot containing the items on which statistics are applied + """ + _StatsWidgetBase.setPlot(self, plot) + self._updateItemObserve() + + def clear(self): + """Define the plot to interact with + + :param Union[PlotWidget,SceneWidget,None] plot: + The plot containing the items on which statistics are applied + """ + self._removeAllItems() + + def _updateItemObserve(self, *args): + """Reload table depending on mode""" + self._removeAllItems() + + # Get selected or all items from the plot + if self._displayOnlyActItem: # Only selected + items = self._plotWrapper.getSelectedItems() + else: # All items + items = self._plotWrapper.getItems() + + # Add items to the plot + for item in items: + self._addItem(item) + + def _plotCurrentChanged(self, current): + """Handle change of current item and update selection in table - self.setRowCount(self.rowCount() + 1) - indexTable = self.rowCount() - 1 - kind = self._getKind(item) - - self._lgdAndKindToItems[(item.getLegend(), kind)] = {} - - # the get item will manage the item creation of not existing - _createItem = self._getItem - for itemName in self._columns: - _createItem(name=itemName, legend=item.getLegend(), kind=kind, - indexTable=indexTable) - - self._updateStats(legend=item.getLegend(), kind=kind) - - callback = functools.partial( - silx.utils.weakref.WeakMethodProxy(self._updateStats), - item.getLegend(), kind) - item.sigItemChanged.connect(callback) - self.setColumnHidden(self._columns_index['kind'], - item.getLegend() not in self._legendsSet) - self._legendsSet.append(item.getLegend()) - - def _getItem(self, name, legend, kind, indexTable): - if (legend, kind) not in self._lgdAndKindToItems: - self._lgdAndKindToItems[(legend, kind)] = {} - if not (name in self._lgdAndKindToItems[(legend, kind)] and - self._lgdAndKindToItems[(legend, kind)]): - if name in ('legend', 'kind'): - _item = qt.QTableWidgetItem(type=qt.QTableWidgetItem.Type) - if name == 'legend': - _item.setText(legend) + :param current: + """ + row = self._itemToRow(current) + if row is None: + if self.currentRow() >= 0: + self.setCurrentCell(-1, -1) + elif row != self.currentRow(): + self.setCurrentCell(row, 0) + + def _tableItemToItem(self, tableItem): + """Find the plot item corresponding to a table item + + :param QTableWidgetItem tableItem: + :rtype: QObject + """ + container = tableItem.data(qt.Qt.UserRole) + return container() + + def _itemToRow(self, item): + """Find the row corresponding to a plot item + + :param item: The plot item + :return: The corresponding row index + :rtype: Union[int,None] + """ + for row in range(self.rowCount()): + tableItem = self.item(row, 0) + if self._tableItemToItem(tableItem) == item: + return row + return None + + def _itemToTableItems(self, item): + """Find all table items corresponding to a plot item + + :param item: The plot item + :return: An ordered dict of column name to QTableWidgetItem mapping + for the given plot item. + :rtype: OrderedDict + """ + result = OrderedDict() + row = self._itemToRow(item) + if row is not None: + for column in range(self.columnCount()): + tableItem = self.item(row, column) + if self._tableItemToItem(tableItem) != item: + _logger.error("Table item/plot item mismatch") else: - assert name == 'kind' - _item.setText(kind) + header = self.horizontalHeaderItem(column) + name = header.data(qt.Qt.UserRole) + result[name] = tableItem + return result + + def _plotItemChanged(self, event): + """Handle modifications of the items. + + :param event: + """ + if self._skipPlotItemChangedEvent(event) is True: + return + else: + item = self.sender() + self._updateStats(item) + + def _addItem(self, item): + """Add a plot item to the table + + If item is not supported, it is ignored. + + :param item: The plot item + :returns: True if the item is added to the widget. + :rtype: bool + """ + if self._itemToRow(item) is not None: + _logger.info("Item already present in the table") + self._updateStats(item) + return True + + kind = self._plotWrapper.getKind(item) + if kind not in statsmdl.BASIC_COMPATIBLE_KINDS: + _logger.info("Item has not a supported type: %s", item) + return False + + # Prepare table items + tableItems = [ + qt.QTableWidgetItem(), # Legend + qt.QTableWidgetItem()] # Kind + + for column in range(2, self.columnCount()): + header = self.horizontalHeaderItem(column) + name = header.data(qt.Qt.UserRole) + + formatter = self._statsHandler.formatters[name] + if formatter: + tableItem = formatter.tabWidgetItemClass() else: - if self._statsHandler.formatters[name]: - _item = self._statsHandler.formatters[name].tabWidgetItemClass() - else: - _item = qt.QTableWidgetItem() - tooltip = self._statsHandler.stats[name].getToolTip(kind=kind) - if tooltip is not None: - _item.setToolTip(tooltip) + tableItem = qt.QTableWidgetItem() - _item.setFlags(qt.Qt.ItemIsEnabled | qt.Qt.ItemIsSelectable) - self.setItem(indexTable, self._columns_index[name], _item) - self._lgdAndKindToItems[(legend, kind)][name] = _item + tooltip = self._statsHandler.stats[name].getToolTip(kind=kind) + if tooltip is not None: + tableItem.setToolTip(tooltip) - return self._lgdAndKindToItems[(legend, kind)][name] + tableItems.append(tableItem) - def _removeItem(self, legend, kind): - if (legend, kind) not in self._lgdAndKindToItems or not self.plot: - return + # Disable sorting while adding table items + with self._disableSorting(): + # Add a row to the table + self.setRowCount(self.rowCount() + 1) - self.firstItem = self._lgdAndKindToItems[(legend, kind)]['legend'] - del self._lgdAndKindToItems[(legend, kind)] - self.removeRow(self.firstItem.row()) - self._legendsSet.remove(legend) - self.setColumnHidden(self._columns_index['kind'], - legend not in self._legendsSet) + # Add table items to the last row + row = self.rowCount() - 1 + for column, tableItem in enumerate(tableItems): + tableItem.setData(qt.Qt.UserRole, _Container(item)) + tableItem.setFlags( + qt.Qt.ItemIsEnabled | qt.Qt.ItemIsSelectable) + self.setItem(row, column, tableItem) - def _updateCurrentStats(self): - for lgdAndKind in self._lgdAndKindToItems: - self._updateStats(lgdAndKind[0], lgdAndKind[1]) + # Update table items content + self._updateStats(item) - def _updateStats(self, legend, kind, event=None): - if self._statsHandler is None: + # Listen for item changes + # Using queued connection to avoid issue with sender + # being that of the signal calling the signal + item.sigItemChanged.connect(self._plotItemChanged, + qt.Qt.QueuedConnection) + + return True + + def _removeItem(self, item): + """Remove table items corresponding to given plot item from the table. + + :param item: The plot item + """ + row = self._itemToRow(item) + if row is None: + kind = self._plotWrapper.getKind(item) + if kind in statsmdl.BASIC_COMPATIBLE_KINDS: + _logger.error("Removing item that is not in table: %s", str(item)) return + item.sigItemChanged.disconnect(self._plotItemChanged) + self.removeRow(row) + + def _removeAllItems(self): + """Remove content of the table""" + for row in range(self.rowCount()): + tableItem = self.item(row, 0) + item = self._tableItemToItem(tableItem) + item.sigItemChanged.disconnect(self._plotItemChanged) + self.clearContents() + self.setRowCount(0) - assert kind in ('curve', 'image', 'scatter', 'histogram') - if kind == 'curve': - item = self.plot.getCurve(legend) - elif kind == 'image': - item = self.plot.getImage(legend) - elif kind == 'scatter': - item = self.plot.getScatter(legend) - elif kind == 'histogram': - item = self.plot.getHistogram(legend) - else: - raise ValueError('kind not managed') + def _updateStats(self, item): + """Update displayed information for given plot item - if not item or (item.getLegend(), kind) not in self._lgdAndKindToItems: + :param item: The plot item + """ + if item is None: + return + plot = self.getPlot() + if plot is None: + _logger.info("Plot not available") return - assert isinstance(item, self.COMPATIBLE_ITEMS) - - statsValDict = self._statsHandler.calculate(item, self.plot, - self._statsOnVisibleData) - - lgdItem = self._lgdAndKindToItems[(item.getLegend(), kind)]['legend'] - assert lgdItem - rowStat = lgdItem.row() - - for statName, statVal in list(statsValDict.items()): - assert statName in self._lgdAndKindToItems[(item.getLegend(), kind)] - tableItem = self._getItem(name=statName, legend=item.getLegend(), - kind=kind, indexTable=rowStat) - tableItem.setText(str(statVal)) - - def currentChanged(self, current, previous): - if current.row() >= 0: - legendItem = self.item(current.row(), self._columns_index['legend']) - assert legendItem - kindItem = self.item(current.row(), self._columns_index['kind']) - kind = kindItem.text() - if kind == 'curve': - self.plot.setActiveCurve(legendItem.text()) - elif kind == 'image': - self.plot.setActiveImage(legendItem.text()) - elif kind == 'scatter': - self.plot._setActiveItem('scatter', legendItem.text()) - elif kind == 'histogram': - # active histogram not managed by the plot actually - pass - else: - raise ValueError('kind not managed') - qt.QTableWidget.currentChanged(self, current, previous) + row = self._itemToRow(item) + if row is None: + _logger.error("This item is not in the table: %s", str(item)) + return - def setDisplayOnlyActiveItem(self, displayOnlyActItem): + statsHandler = self.getStatsHandler() + if statsHandler is not None: + stats = statsHandler.calculate( + item, plot, self._statsOnVisibleData) + else: + stats = {} + + with self._disableSorting(): + for name, tableItem in self._itemToTableItems(item).items(): + if name == self._LEGEND_HEADER_DATA: + text = self._plotWrapper.getLabel(item) + tableItem.setText(text) + elif name == self._KIND_HEADER_DATA: + tableItem.setText(self._plotWrapper.getKind(item)) + else: + value = stats.get(name) + if value is None: + _logger.error("Value not found for: %s", name) + tableItem.setText('-') + else: + tableItem.setText(str(value)) + + def _updateAllStats(self): + """Update stats for all rows in the table""" + with self._disableSorting(): + for row in range(self.rowCount()): + tableItem = self.item(row, 0) + item = self._tableItemToItem(tableItem) + self._updateStats(item) + + def _currentItemChanged(self, current, previous): + """Handle change of selection in table and sync plot selection + + :param QTableWidgetItem current: + :param QTableWidgetItem previous: """ + if current and current.row() >= 0: + item = self._tableItemToItem(current) + self._plotWrapper.setCurrentItem(item) - :param bool displayOnlyActItem: True if we want to only show active - item + def setDisplayOnlyActiveItem(self, displayOnlyActItem): + """Toggle display off all items or only the active/selected one + + :param bool displayOnlyActItem: + True if we want to only show active item """ if self._displayOnlyActItem == displayOnlyActItem: return - self._displayOnlyActItem = displayOnlyActItem self._dealWithPlotConnection(create=False) + if not self._displayOnlyActItem: + self.currentItemChanged.disconnect(self._currentItemChanged) + + _StatsWidgetBase.setDisplayOnlyActiveItem(self, displayOnlyActItem) + self._updateItemObserve() self._dealWithPlotConnection(create=True) + if not self._displayOnlyActItem: + self.currentItemChanged.connect(self._currentItemChanged) + self.setSelectionMode(qt.QAbstractItemView.SingleSelection) + else: + self.setSelectionMode(qt.QAbstractItemView.NoSelection) + + +class _OptionsWidget(qt.QToolBar): + + def __init__(self, parent=None): + qt.QToolBar.__init__(self, parent) + self.setIconSize(qt.QSize(16, 16)) + + action = qt.QAction(self) + action.setIcon(icons.getQIcon("stats-active-items")) + action.setText("Active items only") + action.setToolTip("Display stats for active items only.") + action.setCheckable(True) + action.setChecked(True) + self.__displayActiveItems = action + + action = qt.QAction(self) + action.setIcon(icons.getQIcon("stats-whole-items")) + action.setText("All items") + action.setToolTip("Display stats for all available items.") + action.setCheckable(True) + self.__displayWholeItems = action + + action = qt.QAction(self) + action.setIcon(icons.getQIcon("stats-visible-data")) + action.setText("Use the visible data range") + action.setToolTip("Use the visible data range.<br/>" + "If activated the data is filtered to only use" + "visible data of the plot." + "The filtering is a data sub-sampling." + "No interpolation is made to fit data to" + "boundaries.") + action.setCheckable(True) + self.__useVisibleData = action + + action = qt.QAction(self) + action.setIcon(icons.getQIcon("stats-whole-data")) + action.setText("Use the full data range") + action.setToolTip("Use the full data range.") + action.setCheckable(True) + action.setChecked(True) + self.__useWholeData = action + + self.addAction(self.__displayWholeItems) + self.addAction(self.__displayActiveItems) + self.addSeparator() + self.addAction(self.__useVisibleData) + self.addAction(self.__useWholeData) + + self.itemSelection = qt.QActionGroup(self) + self.itemSelection.setExclusive(True) + self.itemSelection.addAction(self.__displayActiveItems) + self.itemSelection.addAction(self.__displayWholeItems) + + self.dataRangeSelection = qt.QActionGroup(self) + self.dataRangeSelection.setExclusive(True) + self.dataRangeSelection.addAction(self.__useWholeData) + self.dataRangeSelection.addAction(self.__useVisibleData) + + def isActiveItemMode(self): + return self.itemSelection.checkedAction() is self.__displayActiveItems + + def isVisibleDataRangeMode(self): + return self.dataRangeSelection.checkedAction() is self.__useVisibleData + + def setVisibleDataRangeModeEnabled(self, enabled): + """Enable/Disable the visible data range mode + + :param bool enabled: True to allow user to choose + stats on visible data + """ + self.__useVisibleData.setEnabled(enabled) + if not enabled: + self.__useWholeData.setChecked(True) + + +class StatsWidget(qt.QWidget): + """ + Widget displaying a set of :class:`Stat` to be displayed on a + :class:`StatsTable` and to be apply on items contained in the :class:`Plot` + Also contains options to: + + * compute statistics on all the data or on visible data only + * show statistics of all items or only the active one + + :param QWidget parent: Qt parent + :param Union[PlotWidget,SceneWidget] plot: + The plot containing items on which we want statistics. + :param StatsHandler stats: + Set the statistics to be displayed and how to format them using + """ + + sigVisibilityChanged = qt.Signal(bool) + """Signal emitted when the visibility of this widget changes. + + It Provides the visibility of the widget. + """ + + NUMBER_FORMAT = '{0:.3f}' + + def __init__(self, parent=None, plot=None, stats=None): + qt.QWidget.__init__(self, parent) + self.setLayout(qt.QVBoxLayout()) + self.layout().setContentsMargins(0, 0, 0, 0) + self._options = _OptionsWidget(parent=self) + self.layout().addWidget(self._options) + self._statsTable = StatsTable(parent=self, plot=plot) + self.setStats(stats) + + self.layout().addWidget(self._statsTable) + + self._options.itemSelection.triggered.connect( + self._optSelectionChanged) + self._options.dataRangeSelection.triggered.connect( + self._optDataRangeChanged) + self._optSelectionChanged() + self._optDataRangeChanged() + + def _getStatsTable(self): + """Returns the :class:`StatsTable` used by this widget. + + :rtype: StatsTable + """ + return self._statsTable + + def showEvent(self, event): + self.sigVisibilityChanged.emit(True) + qt.QWidget.showEvent(self, event) + + def hideEvent(self, event): + self.sigVisibilityChanged.emit(False) + qt.QWidget.hideEvent(self, event) + + def _optSelectionChanged(self, action=None): + self._getStatsTable().setDisplayOnlyActiveItem( + self._options.isActiveItemMode()) + + def _optDataRangeChanged(self, action=None): + self._getStatsTable().setStatsOnVisibleData( + self._options.isVisibleDataRangeMode()) + + # Proxy methods + + def setStats(self, statsHandler): + return self._getStatsTable().setStats(statsHandler=statsHandler) + + setStats.__doc__ = StatsTable.setStats.__doc__ + + def setPlot(self, plot): + self._options.setVisibleDataRangeModeEnabled( + plot is None or isinstance(plot, PlotWidget)) + return self._getStatsTable().setPlot(plot=plot) + + setPlot.__doc__ = StatsTable.setPlot.__doc__ + + def getPlot(self): + return self._getStatsTable().getPlot() + + getPlot.__doc__ = StatsTable.getPlot.__doc__ + + def setDisplayOnlyActiveItem(self, displayOnlyActItem): + return self._getStatsTable().setDisplayOnlyActiveItem( + displayOnlyActItem=displayOnlyActItem) + + setDisplayOnlyActiveItem.__doc__ = StatsTable.setDisplayOnlyActiveItem.__doc__ + def setStatsOnVisibleData(self, b): + return self._getStatsTable().setStatsOnVisibleData(b=b) + + setStatsOnVisibleData.__doc__ = StatsTable.setStatsOnVisibleData.__doc__ + + +DEFAULT_STATS = StatsHandler(( + (statsmdl.StatMin(), StatFormatter()), + statsmdl.StatCoordMin(), + (statsmdl.StatMax(), StatFormatter()), + statsmdl.StatCoordMax(), + statsmdl.StatCOM(), + (('mean', numpy.mean), StatFormatter()), + (('std', numpy.std), StatFormatter()), +)) + + +class BasicStatsWidget(StatsWidget): + """ + Widget defining a simple set of :class:`Stat` to be displayed on a + :class:`StatsWidget`. + + :param QWidget parent: Qt parent + :param PlotWidget plot: + The plot containing items on which we want statistics. + :param StatsHandler stats: + Set the statistics to be displayed and how to format them using + + .. snapshotqt:: img/BasicStatsWidget.png + :width: 300px + :align: center + + from silx.gui.plot import Plot1D + from silx.gui.plot.StatsWidget import BasicStatsWidget + + plot = Plot1D() + x = range(100) + y = x + plot.addCurve(x, y, legend='curve_0') + plot.setActiveCurve('curve_0') + + widget = BasicStatsWidget(plot=plot) + widget.show() + """ + def __init__(self, parent=None, plot=None): + StatsWidget.__init__(self, parent=parent, plot=plot, + stats=DEFAULT_STATS) + + +class _BaseLineStatsWidget(_StatsWidgetBase, qt.QWidget): + """ + Widget made to display stats into a QLayout with for all stat a couple + (QLabel, QLineEdit) created. + The the layout can be defined prior of adding any statistic. + + :param QWidget parent: Qt parent + :param Union[PlotWidget,SceneWidget] plot: + The plot containing items on which we want statistics. + :param str kind: the kind of plotitems we want to display + :param StatsHandler stats: + Set the statistics to be displayed and how to format them using + :param bool statsOnVisibleData: compute statistics for the whole data or + only visible ones. + """ + + def __init__(self, parent=None, plot=None, kind='curve', stats=None, + statsOnVisibleData=False): + self._item_kind = kind + """The item displayed""" + self._statQlineEdit = {} + """list of legends actually displayed""" + self._n_statistics_per_line = 4 + """number of statistics displayed per line in the grid layout""" + qt.QWidget.__init__(self, parent) + _StatsWidgetBase.__init__(self, + statsOnVisibleData=statsOnVisibleData, + displayOnlyActItem=True) + self.setLayout(self._createLayout()) + self.setPlot(plot) + if stats is not None: + self.setStats(stats) + + def _addItemForStatistic(self, statistic): + assert isinstance(statistic, statsmdl.StatBase) + assert statistic.name in self._statsHandler.stats + + self.layout().setSpacing(2) + self.layout().setContentsMargins(2, 2, 2, 2) + + if isinstance(self.layout(), qt.QGridLayout): + parent = self + else: + widget = qt.QWidget(parent=self) + parent = widget + + qLabel = qt.QLabel(statistic.name + ':', parent=parent) + qLineEdit = qt.QLineEdit('', parent=parent) + qLineEdit.setReadOnly(True) + + self._addStatsWidgetsToLayout(qLabel=qLabel, qLineEdit=qLineEdit) + self._statQlineEdit[statistic.name] = qLineEdit + + def setPlot(self, plot): + """Define the plot to interact with + + :param Union[PlotWidget,SceneWidget,None] plot: + The plot containing the items on which statistics are applied """ - .. warning:: When visible data is activated we will process to a simple - filtering of visible data by the user. The filtering is a - simple data sub-sampling. No interpolation is made to fit - data to boundaries. + _StatsWidgetBase.setPlot(self, plot) + self._updateAllStats() - :param bool b: True if we want to apply statistics only on visible data + def _addStatsWidgetsToLayout(self, qLabel, qLineEdit): + raise NotImplementedError('Base class') + + def setStats(self, statsHandler): + """Set which stats to display and the associated formatting. + :param StatsHandler statsHandler: + Set the statistics to be displayed and how to format them using """ - if self._statsOnVisibleData != b: - self._statsOnVisibleData = b - self._updateCurrentStats() + _StatsWidgetBase.setStats(self, statsHandler) + for statName, stat in list(self._statsHandler.stats.items()): + self._addItemForStatistic(stat) + self._updateAllStats() def _activeItemChanged(self, kind, previous, current): - """Callback used when plotting only the active item""" - assert kind in ('curve', 'image', 'scatter', 'histogram') - self._updateItemObserve() + if kind == self._item_kind: + self._updateAllStats() - def _plotContentChanged(self, action, kind, legend): - """Callback used when plotting all the plot items""" - if kind not in ('curve', 'image', 'scatter', 'histogram'): - return - if kind == 'curve': - item = self.plot.getCurve(legend) - elif kind == 'image': - item = self.plot.getImage(legend) - elif kind == 'scatter': - item = self.plot.getScatter(legend) - elif kind == 'histogram': - item = self.plot.getHistogram(legend) - else: - raise ValueError('kind not managed') + def _updateAllStats(self): + plot = self.getPlot() + if plot is not None: + _items = self._plotWrapper.getSelectedItems() + def kind_filter(_item): + return self._plotWrapper.getKind(_item) == self.getKind() + + items = list(filter(kind_filter, _items)) + assert len(items) in (0, 1) + if len(items) is 1: + self._setItem(items[0]) + + def setKind(self, kind): + """Change the kind of active item to display + :param str kind: kind of item to display information for ('curve' ...) + """ + if self._item_kind != kind: + self._item_kind = kind + self._updateItemObserve() - if action == 'add': - if item is None: - raise ValueError('Item from legend "%s" do not exists' % legend) - self._addItem(item) - elif action == 'remove': - self._removeItem(legend, kind) + def getKind(self): + """ + :return: kind of item we want to compute statistic for + :rtype: str + """ + return self._item_kind + + def _setItem(self, item): + if item is None: + for stat_name, stat_widget in self._statQlineEdit.items(): + stat_widget.setText('') + elif (self._statsHandler is not None and len( + self._statsHandler.stats) > 0): + plot = self.getPlot() + if plot is not None: + statsValDict = self._statsHandler.calculate(item, + plot, + self._statsOnVisibleData) + for statName, statVal in list(statsValDict.items()): + self._statQlineEdit[statName].setText(statVal) + + def _updateItemObserve(self, *argv): + assert self._displayOnlyActItem + _items = self._plotWrapper.getSelectedItems() + def kind_filter(_item): + return self._plotWrapper.getKind(_item) == self.getKind() + items = list(filter(kind_filter, _items)) + assert len(items) in (0, 1) + _item = items[0] if len(items) is 1 else None + self._setItem(_item) + + def _createLayout(self): + """create an instance of the main QLayout""" + raise NotImplementedError('Base class') + + def _addItem(self, item): + raise NotImplementedError('Display only the active item') + + def _removeItem(self, item): + raise NotImplementedError('Display only the active item') + + def _plotCurrentChanged(selfself, current): + raise NotImplementedError('Display only the active item') + + +class BasicLineStatsWidget(_BaseLineStatsWidget): + """ + Widget defining a simple set of :class:`Stat` to be displayed on a + :class:`LineStatsWidget`. + + :param QWidget parent: Qt parent + :param Union[PlotWidget,SceneWidget] plot: + The plot containing items on which we want statistics. + :param str kind: the kind of plotitems we want to display + :param StatsHandler stats: + Set the statistics to be displayed and how to format them using + :param bool statsOnVisibleData: compute statistics for the whole data or + only visible ones. + """ + + def __init__(self, parent=None, plot=None, kind='curve', + stats=DEFAULT_STATS, statsOnVisibleData=False): + _BaseLineStatsWidget.__init__(self, parent=parent, kind=kind, + plot=plot, stats=stats, + statsOnVisibleData=statsOnVisibleData) + + def _createLayout(self): + return FlowLayout() + + def _addStatsWidgetsToLayout(self, qLabel, qLineEdit): + # create a mother widget to make sure both qLabel & qLineEdit will + # always be displayed side by side + widget = qt.QWidget(parent=self) + widget.setLayout(qt.QHBoxLayout()) + widget.layout().setSpacing(0) + widget.layout().setContentsMargins(0, 0, 0, 0) + + widget.layout().addWidget(qLabel) + widget.layout().addWidget(qLineEdit) + + self.layout().addWidget(widget) + + +class BasicGridStatsWidget(_BaseLineStatsWidget): + """ + pymca design like widget + + :param QWidget parent: Qt parent + :param Union[PlotWidget,SceneWidget] plot: + The plot containing items on which we want statistics. + :param StatsHandler stats: + Set the statistics to be displayed and how to format them using + :param str kind: the kind of plotitems we want to display + :param bool statsOnVisibleData: compute statistics for the whole data or + only visible ones. + :param int statsPerLine: number of statistic to be displayed per line + + .. snapshotqt:: img/BasicGridStatsWidget.png + :width: 600px + :align: center + + from silx.gui.plot import Plot1D + from silx.gui.plot.StatsWidget import BasicGridStatsWidget + + plot = Plot1D() + x = range(100) + y = x + plot.addCurve(x, y, legend='curve_0') + plot.setActiveCurve('curve_0') + + widget = BasicGridStatsWidget(plot=plot, kind='curve') + widget.show() + """ - def _zoomPlotChanged(self, event): - if self._statsOnVisibleData is True: - if 'event' in event and event['event'] == 'limitsChanged': - self._updateCurrentStats() + def __init__(self, parent=None, plot=None, kind='curve', + stats=DEFAULT_STATS, statsOnVisibleData=False, + statsPerLine=4): + _BaseLineStatsWidget.__init__(self, parent=parent, kind=kind, + plot=plot, stats=stats, + statsOnVisibleData=statsOnVisibleData) + self._n_statistics_per_line = statsPerLine + + def _addStatsWidgetsToLayout(self, qLabel, qLineEdit): + column = len(self._statQlineEdit) % self._n_statistics_per_line + row = len(self._statQlineEdit) // self._n_statistics_per_line + self.layout().addWidget(qLabel, row, column * 2) + self.layout().addWidget(qLineEdit, row, column * 2 + 1) + + def _createLayout(self): + return qt.QGridLayout() |