class TestRangeSlider(TestCaseQt, ParametricTestCase): """Tests for TestRangeSlider""" def setUp(self): self.slider = RangeSlider() self.slider.show() self.qWaitForWindowExposed(self.slider) def tearDown(self): self.slider.setAttribute(qt.Qt.WA_DeleteOnClose) self.slider.close() del self.slider self.qapp.processEvents() def testRangeValue(self): """Test slider range and values""" # Play with range self.slider.setRange(1, 2) self.assertEqual(self.slider.getRange(), (1., 2.)) self.assertEqual(self.slider.getValues(), (1., 1.)) self.slider.setMinimum(-1) self.assertEqual(self.slider.getRange(), (-1., 2.)) self.assertEqual(self.slider.getValues(), (1., 1.)) self.slider.setMaximum(0) self.assertEqual(self.slider.getRange(), (-1., 0.)) self.assertEqual(self.slider.getValues(), (0., 0.)) # Play with values self.slider.setFirstValue(-2.) self.assertEqual(self.slider.getValues(), (-1., 0.)) self.slider.setFirstValue(-0.5) self.assertEqual(self.slider.getValues(), (-0.5, 0.)) self.slider.setSecondValue(2.) self.assertEqual(self.slider.getValues(), (-0.5, 0.)) self.slider.setSecondValue(-0.1) self.assertEqual(self.slider.getValues(), (-0.5, -0.1)) def testStepCount(self): """Test related to step count""" self.slider.setPositionCount(11) self.assertEqual(self.slider.getPositionCount(), 11) self.slider.setFirstValue(0.32) self.assertEqual(self.slider.getFirstValue(), 0.3) self.assertEqual(self.slider.getFirstPosition(), 3) self.slider.setPositionCount(3) # Value is adjusted self.assertEqual(self.slider.getValues(), (0.5, 1.)) self.assertEqual(self.slider.getPositions(), (1, 2)) def testGroove(self): """Test Groove pixmap""" profile = list(range(100)) for cmap in ('jet', colors.Colormap('viridis')): with self.subTest(str(cmap)): self.slider.setGroovePixmapFromProfile(profile, cmap) pixmap = self.slider.getGroovePixmap() self.assertIsInstance(pixmap, qt.QPixmap) self.assertEqual(pixmap.width(), len(profile))
class HistogramWidget(qt.QWidget): """Widget displaying a histogram and some statistic indicators""" _SUPPORTED_ITEM_CLASS = items.ImageBase, items.Scatter def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setWindowTitle('Histogram') self.__itemRef = None # weakref on the item to track layout = qt.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) # Plot # Lazy import to avoid circular dependencies from silx.gui.plot.PlotWindow import Plot1D self.__plot = Plot1D(self) layout.addWidget(self.__plot) self.__plot.setDataMargins(0.1, 0.1, 0.1, 0.1) self.__plot.getXAxis().setLabel("Value") self.__plot.getYAxis().setLabel("Count") posInfo = self.__plot.getPositionInfoWidget() posInfo.setSnappingMode(posInfo.SNAPPING_CURVE) # Histogram controls controlsWidget = qt.QWidget(self) layout.addWidget(controlsWidget) controlsLayout = qt.QHBoxLayout(controlsWidget) controlsLayout.setContentsMargins(4, 4, 4, 4) controlsLayout.addWidget(qt.QLabel("<b>Histogram:<b>")) controlsLayout.addWidget(qt.QLabel("N. bins:")) self.__nbinsLineEdit = _IntEdit(self) self.__nbinsLineEdit.setRange(2, 9999) self.__nbinsLineEdit.sigValueChanged.connect( self.__updateHistogramFromControls) controlsLayout.addWidget(self.__nbinsLineEdit) self.__rangeLabel = qt.QLabel("Range:") controlsLayout.addWidget(self.__rangeLabel) self.__rangeSlider = RangeSlider(parent=self) self.__rangeSlider.sigValueChanged.connect( self.__updateHistogramFromControls) self.__rangeSlider.sigValueChanged.connect(self.__rangeChanged) controlsLayout.addWidget(self.__rangeSlider) controlsLayout.addStretch(1) # Stats display statsWidget = qt.QWidget(self) layout.addWidget(statsWidget) statsLayout = qt.QHBoxLayout(statsWidget) statsLayout.setContentsMargins(4, 4, 4, 4) self.__statsWidgets = dict( (name, _StatWidget(parent=statsWidget, name=name)) for name in ("min", "max", "mean", "std", "sum")) for widget in self.__statsWidgets.values(): statsLayout.addWidget(widget) statsLayout.addStretch(1) def getPlotWidget(self): """Returns :class:`PlotWidget` use to display the histogram""" return self.__plot def resetZoom(self): """Reset PlotWidget zoom""" self.getPlotWidget().resetZoom() def reset(self): """Clear displayed information""" self.getPlotWidget().clear() self.setStatistics() def getItem(self) -> Optional[items.Item]: """Returns item used to display histogram and statistics.""" return None if self.__itemRef is None else self.__itemRef() def setItem(self, item: Optional[items.Item]): """Set item from which to display histogram and statistics. :param item: """ previous = self.getItem() if previous is not None: previous.sigItemChanged.disconnect(self.__itemChanged) self.__itemRef = None if item is None else weakref.ref(item) if item is not None: if isinstance(item, self._SUPPORTED_ITEM_CLASS): # Only listen signal for supported items item.sigItemChanged.connect(self.__itemChanged) self._updateFromItem() def __itemChanged(self, event): """Handle update of the item""" if event in (items.ItemChangedType.DATA, items.ItemChangedType.MASK): self._updateFromItem() def __updateHistogramFromControls(self, *args): """Handle udates coming from histogram control widgets""" hist = self.getHistogram(copy=False) if hist is not None: count, edges = hist if (len(count) == self.__nbinsLineEdit.getValue() and (edges[0], edges[-1]) == self.__rangeSlider.getValues()): return # Nothing has changed self._updateFromItem() def __rangeChanged(self, first, second): """Handle change of histogram range from the range slider""" tooltip = "Histogram range:\n[%g, %g]" % (first, second) self.__rangeSlider.setToolTip(tooltip) self.__rangeLabel.setToolTip(tooltip) def _updateFromItem(self): """Update histogram and stats from the item""" item = self.getItem() if item is None: self.reset() return if not isinstance(item, self._SUPPORTED_ITEM_CLASS): _logger.error("Unsupported item", item) self.reset() return # Compute histogram and stats array = item.getValueData(copy=False) if array.size == 0: self.reset() return xmin, xmax = min_max(array, min_positive=False, finite=True) if xmin is None or xmax is None: # All not finite data self.reset() return guessed_nbins = min(1024, int(numpy.sqrt(array.size))) # bad hack: get 256 bins in the case we have a B&W if numpy.issubdtype(array.dtype, numpy.integer): if guessed_nbins > xmax - xmin: guessed_nbins = xmax - xmin guessed_nbins = max(2, guessed_nbins) # Set default nbins self.__nbinsLineEdit.setDefaultValue(guessed_nbins, extend_range=True) # Set slider range: do not keep the range value, but the relative pos. previousPositions = self.__rangeSlider.getPositions() if xmin == xmax: # Enlarge range is none if xmin == 0: range_ = -0.01, 0.01 else: range_ = sorted((xmin * .99, xmin * 1.01)) else: range_ = xmin, xmax self.__rangeSlider.setRange(*range_) self.__rangeSlider.setPositions(*previousPositions) histogram = Histogramnd( array.ravel().astype(numpy.float32), n_bins=max(2, self.__nbinsLineEdit.getValue()), histo_range=self.__rangeSlider.getValues(), ) if len(histogram.edges) != 1: _logger.error("Error while computing the histogram") self.reset() return self.setHistogram(histogram.histo, histogram.edges[0]) self.resetZoom() self.setStatistics(min_=xmin, max_=xmax, mean=numpy.nanmean(array), std=numpy.nanstd(array), sum_=numpy.nansum(array)) def setHistogram(self, histogram, edges): """Set displayed histogram :param histogram: Bin values (N) :param edges: Bin edges (N+1) """ # Only useful if setHistogram is called directly # TODO #nbins = len(histogram) #if nbins != self.__nbinsLineEdit.getDefaultValue(): # self.__nbinsLineEdit.setValue(nbins, extend_range=True) #self.__rangeSlider.setValues(edges[0], edges[-1]) self.getPlotWidget().addHistogram(histogram=histogram, edges=edges, legend='histogram', fill=True, color='#66aad7', resetzoom=False) def getHistogram(self, copy: bool = True): """Returns currently displayed histogram. :param copy: True to get a copy, False to get internal representation (Do not modify!) :return: (histogram, edges) or None """ for item in self.getPlotWidget().getItems(): if item.getName() == 'histogram': return (item.getValueData(copy=copy), item.getBinEdgesData(copy=copy)) else: return None def setStatistics(self, min_: Optional[float] = None, max_: Optional[float] = None, mean: Optional[float] = None, std: Optional[float] = None, sum_: Optional[float] = None): """Set displayed statistic indicators.""" self.__statsWidgets['min'].setValue(min_) self.__statsWidgets['max'].setValue(max_) self.__statsWidgets['mean'].setValue(mean) self.__statsWidgets['std'].setValue(std) self.__statsWidgets['sum'].setValue(sum_)