summaryrefslogtreecommitdiff
path: root/silx/gui/plot/StatsWidget.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot/StatsWidget.py')
-rw-r--r--silx/gui/plot/StatsWidget.py1594
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()