summaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
authorPicca Frédéric-Emmanuel <picca@synchrotron-soleil.fr>2018-07-31 16:22:25 +0200
committerPicca Frédéric-Emmanuel <picca@synchrotron-soleil.fr>2018-07-31 16:22:25 +0200
commit159ef14fb9e198bb0066ea14e6b980f065de63dd (patch)
treebc37c7d4ba09ee59deb708897fa0571709aec293 /examples
parent270d5ddc31c26b62379e3caa9044dd75ccc71847 (diff)
New upstream version 0.8.0+dfsg
Diffstat (limited to 'examples')
-rw-r--r--examples/blissPlot.py635
-rw-r--r--examples/colormapDialog.py8
-rw-r--r--examples/findContours.py704
-rwxr-xr-xexamples/hdf5widget.py10
-rwxr-xr-xexamples/imageview.py1
-rw-r--r--examples/plot3dSceneWindow.py5
-rw-r--r--examples/plotGL32.py46
-rw-r--r--examples/plotInteractiveImageROI.py110
-rw-r--r--examples/plotStats.py123
-rw-r--r--examples/plotTimeSeries.py33
-rw-r--r--examples/plotUpdateCurveFromThread.py (renamed from examples/plotUpdateFromThread.py)64
-rw-r--r--examples/plotUpdateImageFromThread.py133
-rw-r--r--examples/plotWidget.py100
-rw-r--r--examples/scatterMask.py6
14 files changed, 1888 insertions, 90 deletions
diff --git a/examples/blissPlot.py b/examples/blissPlot.py
new file mode 100644
index 0000000..71c1fd3
--- /dev/null
+++ b/examples/blissPlot.py
@@ -0,0 +1,635 @@
+# coding: utf-8
+
+
+from __future__ import division, absolute_import, print_function, unicode_literals
+
+
+import six
+
+from silx.gui import qt, icons
+from silx.gui.plot.actions import PlotAction, mode
+from silx.gui.plot import PlotWindow, PlotWidget
+from silx.gui.plot.Colors import rgba
+
+
+class DrawModeAction(PlotAction):
+ """Action that control drawing mode"""
+
+ _MODES = { # shape: (icon, text, tooltip
+ 'rectangle': ('shape-rectangle', 'Rectangle selection', 'Select a rectangular region'),
+ 'line': ('shape-diagonal', 'Line selection', 'Select a line'),
+ 'hline': ('shape-horizontal', 'H. line selection', 'Select a horizontal line'),
+ 'vline': ('shape-vertical', 'V. line selection', 'Select a vertical line'),
+ 'polygon': ('shape-polygon', 'Polygon selection', 'Select a polygon'),
+ }
+
+ def __init__(self, plot, parent=None):
+ self._shape = 'polygon'
+ self._label = None
+ self._color = 'black'
+ self._width = None
+ icon, text, tooltip = self._MODES[self._shape]
+
+ super(DrawModeAction, self).__init__(
+ plot, icon=icon, text=text,
+ tooltip=tooltip,
+ triggered=self._actionTriggered,
+ checkable=True, parent=parent)
+
+ # Listen to mode change
+ self.plot.sigInteractiveModeChanged.connect(self._modeChanged)
+ # Init the state
+ self._modeChanged(None)
+
+ def _update(self):
+ if self.isChecked():
+ self._actionTriggered()
+
+ def setShape(self, shape):
+ self._shape = shape
+ icon, text, tooltip = self._MODES[self._shape]
+ self.setIcon(icons.getQIcon(icon))
+ self.setText(text)
+ self.setToolTip(tooltip)
+ self._update()
+
+ def getShape(self):
+ return self._shape
+
+ def setColor(self, color):
+ self._color = rgba(color)
+ self._update()
+
+ def getColor(self):
+ return qt.QColor.fromRgbF(*self._color)
+
+ def setLabel(self, label):
+ self._label = label
+ self._update()
+
+ def getLabel(self):
+ return self._label
+
+ def setWidth(self, width):
+ self._width = width
+ self._update()
+
+ def getWidth(self):
+ return self._width
+
+ def _modeChanged(self, source):
+ modeDict = self.plot.getInteractiveMode()
+ old = self.blockSignals(True)
+ self.setChecked(modeDict['mode'] == 'draw' and
+ modeDict['shape'] == self._shape and
+ modeDict['label'] == self._label)
+ self.blockSignals(old)
+
+ def _actionTriggered(self, checked=False):
+ self.plot.setInteractiveMode('draw',
+ source=self,
+ shape=self._shape,
+ color=self._color,
+ label=self._label,
+ width=self._width)
+
+
+class ShapeSelector(qt.QObject):
+ """Handles the selection of a single shape in a PlotWidget
+
+ :param parent: QObject's parent
+ """
+
+ selectionChanged = qt.Signal(tuple)
+ """Signal emitted whenever the selection has changed.
+
+ It provides the selection.
+ """
+
+ selectionFinished = qt.Signal(tuple)
+ """Signal emitted when selection is terminated.
+
+ It provides the selection.
+ """
+
+ def __init__(self, parent=None):
+ assert isinstance(parent, PlotWidget)
+ super(ShapeSelector, self).__init__(parent)
+ self._isSelectionRunning = False
+ self._selection = ()
+ self._itemId = "%s-%s" % (self.__class__.__name__, id(self))
+
+ # Add a toolbar to plot
+ self._toolbar = qt.QToolBar('Selection')
+ self._modeAction = DrawModeAction(plot=parent)
+ self._modeAction.setLabel(self._itemId)
+ self._modeAction.setColor(rgba('red'))
+ toolButton = qt.QToolButton()
+ toolButton.setDefaultAction(self._modeAction)
+ toolButton.setToolButtonStyle(qt.Qt.ToolButtonTextBesideIcon)
+ self._toolbar.addWidget(toolButton)
+
+ # Style
+
+ def getColor(self):
+ """Returns the color used for the selection shape
+
+ :rtype: QColor
+ """
+ return self._modeAction.getColor()
+
+ def setColor(self, color):
+ """Set the color used for the selection shape
+
+ :param color: The color to use for selection shape as
+ either a color name, a QColor, a list of uint8 or float in [0, 1].
+ """
+ self._modeAction.setColor(color)
+ self._updateShape()
+
+ # Control selection
+
+ def getSelection(self):
+ """Returns selection control point coordinates
+
+ Returns an empty tuple if there is no selection
+
+ :return: Nx2 (x, y) coordinates or an empty tuple.
+ """
+ return tuple(zip(*self._selection))
+
+ def _setSelection(self, x, y):
+ """Set the selection shape control points.
+
+ Use :meth:`reset` to remove the selection.
+
+ :param x: X coordinates of control points
+ :param y: Y coordinates of control points
+ """
+ selection = x, y
+ if selection != self._selection:
+ self._selection = selection
+ self._updateShape()
+ self.selectionChanged.emit(self.getSelection())
+
+ def reset(self):
+ """Clear the rectangle selection"""
+ if self._selection:
+ self._selection = ()
+ self._updateShape()
+ self.selectionChanged.emit(self.getSelection())
+
+ def start(self, shape):
+ """Start requiring user to select a rectangle
+
+ :param str shape: The shape to select in:
+ 'rectangle', 'line', 'polygon', 'hline', 'vline'
+ """
+ plot = self.parent()
+ if plot is None:
+ raise RuntimeError('No plot to perform selection')
+
+ self.stop()
+ self.reset()
+
+ assert shape in ('rectangle', 'line', 'polygon', 'hline', 'vline')
+
+ self._modeAction.setShape(shape)
+ self._modeAction.trigger() # To set the interaction mode
+
+ self._isSelectionRunning = True
+
+ plot.sigPlotSignal.connect(self._handleDraw)
+
+ self._toolbar.show()
+ plot.addToolBar(qt.Qt.BottomToolBarArea, self._toolbar)
+
+ def stop(self):
+ """Stop shape selection"""
+ if not self._isSelectionRunning:
+ return
+
+ plot = self.parent()
+ if plot is None:
+ return
+
+ mode = plot.getInteractiveMode()
+ if mode['mode'] == 'draw' and mode['label'] == self._itemId:
+ plot.setInteractiveMode('zoom') # This disconnects draw handler
+
+ plot.sigPlotSignal.disconnect(self._handleDraw)
+
+ plot.removeToolBar(self._toolbar)
+
+ self._isSelectionRunning = False
+ self.selectionFinished.emit(self.getSelection())
+
+ def _handleDraw(self, event):
+ """Handle shape drawing event"""
+ if (event['event'] == 'drawingFinished' and
+ event['parameters']['label'] == self._itemId):
+ self._setSelection(event['xdata'], event['ydata'])
+ self.stop()
+
+ def _updateShape(self):
+ """Update shape on the plot"""
+ plot = self.parent()
+ if plot is not None:
+ if not self._selection:
+ plot.remove(legend=self._itemId, kind='item')
+
+ else:
+ x, y = self._selection
+ shape = self._modeAction.getShape()
+ if shape == 'line':
+ shape = 'polylines'
+
+ plot.addItem(x, y,
+ legend=self._itemId,
+ shape=shape,
+ color=rgba(self._modeAction.getColor()),
+ fill=False)
+
+
+
+class PointsSelector(qt.QObject):
+ """Handle selection of points in a PlotWidget"""
+
+ selectionChanged = qt.Signal(tuple)
+ """Signal emitted whenever the selection has changed.
+
+ It provides the selection.
+ """
+
+ selectionFinished = qt.Signal(tuple)
+ """Signal emitted when selection is terminated.
+
+ It provides the selection.
+ """
+
+
+ def __init__(self, parent):
+ assert isinstance(parent, PlotWidget)
+ super(PointsSelector, self).__init__(parent)
+
+ self._isSelectionRunning = False
+ self._markersAndPos = []
+ self._totalPoints = 0
+
+ def getSelection(self):
+ """Returns the selection"""
+ return tuple(pos for _, pos in self._markersAndPos)
+
+ def eventFilter(self, obj, event):
+ """Event filter for plot hide and key event"""
+ if event.type() == qt.QEvent.Hide:
+ self.stop()
+
+ elif event.type() == qt.QEvent.KeyPress:
+ if event.key() in (qt.Qt.Key_Delete, qt.Qt.Key_Backspace) or (
+ event.key() == qt.Qt.Key_Z and event.modifiers() & qt.Qt.ControlModifier):
+ if len(self._markersAndPos) > 0:
+ plot = self.parent()
+ if plot is not None:
+ legend, _ = self._markersAndPos.pop()
+ plot.remove(legend=legend, kind='marker')
+
+ self._updateStatusBar()
+ self.selectionChanged.emit(self.getSelection())
+ return True # Stop further handling of those keys
+
+ elif event.key() == qt.Qt.Key_Return:
+ self.stop()
+ return True # Stop further handling of those keys
+
+ return super(PointsSelector, self).eventFilter(obj, event)
+
+ def start(self, nbPoints=1):
+ """Start interactive selection of points
+
+ :param int nbPoints: Number of points to select
+ """
+ self.stop()
+ self.reset()
+
+ plot = self.parent()
+ if plot is None:
+ raise RuntimeError('No plot to perform selection')
+
+ self._totalPoints = nbPoints
+ self._isSelectionRunning = True
+
+ plot.setInteractiveMode(mode='zoom')
+ self._handleInteractiveModeChanged(None)
+ plot.sigInteractiveModeChanged.connect(
+ self._handleInteractiveModeChanged)
+
+ plot.installEventFilter(self)
+
+ self._updateStatusBar()
+
+ def stop(self):
+ """Stop interactive point selection"""
+ if not self._isSelectionRunning:
+ return
+
+ plot = self.parent()
+ if plot is None:
+ return
+
+ plot.removeEventFilter(self)
+
+ plot.sigInteractiveModeChanged.disconnect(
+ self._handleInteractiveModeChanged)
+
+ currentMode = plot.getInteractiveMode()
+ if currentMode['mode'] == 'zoom': # Stop handling mouse click
+ plot.sigPlotSignal.disconnect(self._handleSelect)
+
+ plot.statusBar().clearMessage()
+ self._isSelectionRunning = False
+ self.selectionFinished.emit(self.getSelection())
+
+ def reset(self):
+ """Reset selected points"""
+ plot = self.parent()
+ if plot is None:
+ return
+
+ for legend, _ in self._markersAndPos:
+ plot.remove(legend=legend, kind='marker')
+ self._markersAndPos = []
+ self.selectionChanged.emit(self.getSelection())
+
+ def _updateStatusBar(self):
+ """Update status bar message"""
+ plot = self.parent()
+ if plot is None:
+ return
+
+ msg = 'Select %d/%d input points' % (len(self._markersAndPos),
+ self._totalPoints)
+
+ currentMode = plot.getInteractiveMode()
+ if currentMode['mode'] != 'zoom':
+ msg += ' (Use zoom mode to add/remove points)'
+
+ plot.statusBar().showMessage(msg)
+
+ def _handleSelect(self, event):
+ """Handle mouse events"""
+ if event['event'] == 'mouseClicked' and event['button'] == 'left':
+ plot = self.parent()
+ if plot is None:
+ return
+
+ x, y = event['x'], event['y']
+
+ # Add marker
+ legend = "sx.ginput %d" % len(self._markersAndPos)
+ plot.addMarker(
+ x, y,
+ legend=legend,
+ text='%d' % len(self._markersAndPos),
+ color='red',
+ draggable=False)
+
+ self._markersAndPos.append((legend, (x, y)))
+ self._updateStatusBar()
+ if len(self._markersAndPos) >= self._totalPoints:
+ self.stop()
+
+ def _handleInteractiveModeChanged(self, source):
+ """Handle change of interactive mode in the plot
+
+ :param source: Objects that triggered the mode change
+ """
+ plot = self.parent()
+ if plot is None:
+ return
+
+ mode = plot.getInteractiveMode()
+ if mode['mode'] == 'zoom': # Handle click events
+ plot.sigPlotSignal.connect(self._handleSelect)
+ else: # Do not handle click event
+ plot.sigPlotSignal.disconnect(self._handleSelect)
+ self._updateStatusBar()
+
+
+# TODO refactor to make a selection by composition rather than inheritance...
+class BlissPlot(PlotWindow):
+ """Plot with selection methods"""
+
+ sigSelectionDone = qt.Signal(object)
+ """Signal emitted when the selection is done
+
+ It provides the list of selected points
+ """
+
+ def __init__(self, parent=None, **kwargs):
+ super(BlissPlot, self).__init__(parent=parent, **kwargs)
+ self._selectionColor = rgba('red')
+ self._selectionMode = None
+ self._markers = []
+ self._pointNames = ()
+
+ # Style
+
+ def getColor(self):
+ """Returns the color used for selection markers
+
+ :rtype: QColor
+ """
+ return qt.QColor.fromRgbF(*self._selectionColor)
+
+ def setColor(self, color):
+ """Set the markers used for selection
+
+ :param color: The color to use for selection markers as
+ either a color name, a QColor, a list of uint8 or float in [0, 1].
+ """
+ self._selectionColor = rgba(color)
+ self._updateMarkers() # To apply color change
+
+ # Marker helpers
+
+ def _setSelectedPointMarker(self, x, y, index=None):
+ """Add/Update a marker for a point
+
+ :param float x: X coord in plot
+ :param float y: Y coord in plot
+ :param int index: Index of point in points names to set
+ :return: corresponding marker legend
+ :rtype: str
+ """
+ if index is None:
+ index = len(self._markers)
+
+ name = self._pointNames[index]
+ legend = "BlissPlotSelection-%d" % index
+
+ self.addMarker(
+ x, y,
+ legend=legend,
+ text=name,
+ color=self._selectionColor,
+ draggable=self._selectionMode is not None)
+ return legend
+
+ def _updateMarkers(self):
+ """Update all markers to sync color/draggable"""
+ for index, (x, y) in enumerate(self.getSelectedPoints()):
+ self._setSelectedPointMarker(x, y, index)
+
+ # Selection mode control
+
+ def startPointSelection(self, points=1):
+ """Request the user to select a number of points
+
+ :param points:
+ The number of points the user need to select (default: 1)
+ or a list of point names or a single name.
+ :type points: Union[int, List[str], str]
+ :return: A future to access the result
+ :rtype: concurrent.futures.Future
+ """
+ self.stopSelection()
+ self.resetSelection()
+
+ if isinstance(points, six.string_types):
+ points = [points]
+ elif isinstance(points, int):
+ points = [str(i) for i in range(points)]
+
+ self._pointNames = points
+
+ self._markers = []
+ self._selectionMode = 'points'
+
+ self.setInteractiveMode(mode='zoom')
+ self._handleInteractiveModeChanged(None)
+ self.sigInteractiveModeChanged.connect(
+ self._handleInteractiveModeChanged)
+
+ def stopSelection(self):
+ """Stop current selection.
+
+ Calling this method emits the selection through sigSelectionDone
+ and does not clear the selection.
+ """
+ if self._selectionMode is not None:
+ currentMode = self.getInteractiveMode()
+ if currentMode['mode'] == 'zoom': # Stop handling mouse click
+ self.sigPlotSignal.disconnect(self._handleSelect)
+
+ self.sigInteractiveModeChanged.disconnect(
+ self._handleInteractiveModeChanged)
+
+ self._selectionMode = None
+ self.statusBar().showMessage('Selection done')
+
+ self._updateMarkers() # To make them not draggable
+
+ self.sigSelectionDone.emit(self.getSelectedPoints())
+
+ def getSelectedPoints(self):
+ """Returns list of currently selected points
+
+ :rtype: tuple
+ """
+ return tuple(self._getItem(kind='marker', legend=legend).getPosition()
+ for legend in self._markers)
+
+ def resetSelection(self):
+ """Clear current selection"""
+ for legend in self._markers:
+ self.remove(legend, kind='marker')
+ self._markers = []
+
+ if self._selectionMode is not None:
+ self._updateStatusBar()
+ else:
+ self.statusBar().clearMessage()
+
+ def _handleInteractiveModeChanged(self, source):
+ """Handle change of interactive mode in the plot
+
+ :param source: Objects that triggered the mode change
+ """
+ mode = self.getInteractiveMode()
+ if mode['mode'] == 'zoom': # Handle click events
+ self.sigPlotSignal.connect(self._handleSelect)
+ else: # Do not handle click event
+ self.sigPlotSignal.disconnect(self._handleSelect)
+ self._updateStatusBar()
+
+ def _handleSelect(self, event):
+ """Handle mouse events"""
+ if event['event'] == 'mouseClicked' and event['button'] == 'left':
+ if len(self._markers) == len(self._pointNames):
+ return
+
+ x, y = event['x'], event['y']
+ legend = self._setSelectedPointMarker(x, y, len(self._markers))
+ self._markers.append(legend)
+ self._updateStatusBar()
+
+ def keyPressEvent(self, event):
+ """Handle keys for undo/done actions"""
+ if self._selectionMode is not None:
+ if event.key() in (qt.Qt.Key_Delete, qt.Qt.Key_Backspace) or (
+ event.key() == qt.Qt.Key_Z and
+ event.modifiers() & qt.Qt.ControlModifier):
+ if len(self._markers) > 0:
+ legend = self._markers.pop()
+ self.remove(legend, kind='marker')
+
+ self._updateStatusBar()
+ return # Stop processing the event
+
+ elif event.key() == qt.Qt.Key_Return:
+ self.stopSelection()
+ return # Stop processing the event
+
+ return super(BlissPlot, self).keyPressEvent(event)
+
+ def _updateStatusBar(self):
+ """Update status bar message"""
+ if len(self._markers) < len(self._pointNames):
+ name = self._pointNames[len(self._markers)]
+ msg = 'Select point: %s (%d/%d)' % (
+ name, len(self._markers), len(self._pointNames))
+ else:
+ msg = 'Selection ready. Press Enter to validate'
+
+ currentMode = self.getInteractiveMode()
+ if currentMode['mode'] != 'zoom':
+ msg += ' (Use zoom mode to add/edit points)'
+
+ self.statusBar().showMessage(msg)
+
+
+if __name__ == '__main__':
+ app = qt.QApplication([])
+
+ #plot = BlissPlot()
+ #plot.startPointSelection(('first', 'second', 'third'))
+
+ def dumpChanged(selection):
+ print('selectionChanged', selection)
+
+ def dumpFinished(selection):
+ print('selectionFinished', selection)
+
+ plot = PlotWindow()
+ selector = ShapeSelector(plot)
+ #selector.start(shape='rectangle')
+ selector.selectionChanged.connect(dumpChanged)
+ selector.selectionFinished.connect(dumpFinished)
+ plot.show()
+
+ points = PointsSelector(plot)
+ points.start(3)
+ points.selectionChanged.connect(dumpChanged)
+ points.selectionFinished.connect(dumpFinished)
+ #app.exec_()
diff --git a/examples/colormapDialog.py b/examples/colormapDialog.py
index c663591..9ef6508 100644
--- a/examples/colormapDialog.py
+++ b/examples/colormapDialog.py
@@ -22,12 +22,12 @@
# THE SOFTWARE.
#
# ###########################################################################*/
-"""This script shows the features of a :mod:`~silx.gui.plot.ColormapDialog`.
+"""This script shows the features of a :mod:`~silx.gui.dialog.ColormapDialog`.
"""
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "19/01/2018"
+__date__ = "14/06/2018"
import functools
import numpy
@@ -38,8 +38,8 @@ except ImportError:
scipy = None
from silx.gui import qt
-from silx.gui.plot.ColormapDialog import ColormapDialog
-from silx.gui.plot.Colormap import Colormap
+from silx.gui.dialog.ColormapDialog import ColormapDialog
+from silx.gui.colors import Colormap
from silx.gui.plot.ColorBar import ColorBarWidget
diff --git a/examples/findContours.py b/examples/findContours.py
new file mode 100644
index 0000000..a5bb663
--- /dev/null
+++ b/examples/findContours.py
@@ -0,0 +1,704 @@
+#!/usr/bin/env python
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""Find contours examples
+
+.. note:: This module has an optional dependancy with sci-kit image library.
+ You might need to install it if you don't already have it.
+"""
+
+import logging
+import sys
+import numpy
+import time
+
+logging.basicConfig()
+_logger = logging.getLogger("find_contours")
+
+from silx.gui import qt
+import silx.gui.plot
+from silx.gui.colors import Colormap
+import silx.image.bilinear
+
+
+try:
+ import skimage
+except ImportError:
+ _logger.debug("Error while importing skimage", exc_info=True)
+ skimage = None
+
+if skimage is not None:
+ try:
+ from silx.image.marchingsquares._skimage import MarchingSquaresSciKitImage
+ except ImportError:
+ _logger.debug("Error while importing MarchingSquaresSciKitImage", exc_info=True)
+ MarchingSquaresSciKitImage = None
+else:
+ MarchingSquaresSciKitImage = None
+
+
+def rescale_image(image, shape):
+ y, x = numpy.ogrid[:shape[0], :shape[1]]
+ y, x = y * 1.0 * (image.shape[0] - 1) / (shape[0] - 1), x * 1.0 * (image.shape[1] - 1) / (shape[1] - 1)
+ b = silx.image.bilinear.BilinearImage(image)
+ # TODO: could be optimized using strides
+ x2d = numpy.zeros_like(y) + x
+ y2d = numpy.zeros_like(x) + y
+ result = b.map_coordinates((y2d, x2d))
+ return result
+
+
+def create_spiral(size, nb=1, freq=100):
+ half = size // 2
+ y, x = numpy.ogrid[-half:half, -half:half]
+ coef = 1.0 / half
+ y, x = y * coef, x * coef + 0.0001
+ distance = numpy.sqrt(x * x + y * y)
+ angle = numpy.arctan(y / x)
+ data = numpy.sin(angle * nb * 2 + distance * freq * half / 100, dtype=numpy.float32)
+ return data
+
+
+def create_magnetic_field(size, x1=0.0, y1=0.0, x2=0.0, y2=0.0):
+ half = size // 2
+ yy, xx = numpy.ogrid[-half:half, -half:half]
+ coef = 1.0 / half
+ yy1, xx1 = (yy + half * y1) * coef, (xx + half * x1) * coef
+ distance1 = numpy.sqrt(xx1 * xx1 + yy1 * yy1)
+ yy2, xx2 = (yy + half * y2) * coef, (xx + half * x2) * coef
+ distance2 = numpy.sqrt(xx2 * xx2 + yy2 * yy2)
+ return (numpy.arctan2(distance1, distance2) - numpy.pi * 0.25) * 1000
+
+
+def create_gravity_field(size, objects):
+ half = size // 2
+ yy, xx = numpy.ogrid[-half:half, -half:half]
+ coef = 1.0 / half
+
+ def distance(x, y):
+ yy1, xx1 = (yy + half * y) * coef, (xx + half * x) * coef
+ return numpy.sqrt(xx1 ** 2 + yy1 ** 2)
+ result = numpy.zeros((size, size), dtype=numpy.float32)
+ for x, y, m in objects:
+ result += m / distance(x, y)
+ return numpy.log(result) * 1000
+
+
+def create_gradient(size, dx=0, dy=0, sx=1.0, sy=1.0):
+ half = size // 2
+ yy, xx = numpy.ogrid[-half:half, -half:half]
+ coef = 1.0 / half
+ yy, xx = (yy - (dy * half)) * coef, (xx - (dx * half)) * coef + 0.0001
+ distance = numpy.sqrt(xx * xx * sx + yy * yy * sy)
+ return distance
+
+
+def create_composite_gradient(size, dx=0, dy=0, sx=1.0, sy=1.0):
+ hole = (size - 4) // 4
+ gap = 10
+ base = create_gradient(size + hole + gap * 4, dx, dy, sx, sy)
+ result = numpy.zeros((size, size))
+ width = (size - 2) // 2
+ half_hole = hole // 2
+
+ def copy_module(x1, y1, x2, y2, width, height):
+ result[y1:y1 + height, x1:x1 + width] = base[y2:y2 + height, x2:x2 + width]
+
+ y1 = 0
+ y2 = 0
+ copy_module(0, y1, half_hole, y2, width, hole)
+ copy_module(width + 1, y1, half_hole + width, y2, width, hole)
+
+ y1 += hole + 1
+ y2 += hole + gap
+ copy_module(0, y1, 0, y2, width, hole)
+ copy_module(width + 1, y1, width + hole, y2, width, hole)
+
+ y1 += hole + 1
+ y2 += hole + gap
+ copy_module(0, y1, half_hole, y2, width, hole)
+ copy_module(width + 1, y1, half_hole + width, y2, width, hole)
+
+ y1 += hole + 1
+ y2 += hole + gap
+ copy_module(0, y1, half_hole, y2, width, hole)
+ copy_module(width + 1, y1, half_hole + width, y2, width, hole)
+
+ return result
+
+
+def create_value_noise(shape, octaves=8, weights=None, first_array=None):
+ data = numpy.zeros(shape, dtype=numpy.float32)
+ t = 2
+ for i in range(octaves):
+ if t > shape[0] and t > shape[1]:
+ break
+ if i == 0 and first_array is not None:
+ d = first_array
+ else:
+ if weights is None:
+ w = (256 >> i) - 1
+ else:
+ w = weights[i]
+ d = numpy.random.randint(w, size=(t, t)).astype(dtype=numpy.uint8)
+ d = rescale_image(d, shape)
+ data = data + d
+ t = t << 1
+ return data
+
+
+def create_island(shape, summit, under_water):
+ # Force a centric shape
+ first_array = numpy.zeros((4, 4), dtype=numpy.uint8)
+ first_array[1:3, 1:3] = 255
+ weights = [255] + [(256 >> (i)) - 1 for i in range(8)]
+ data = create_value_noise(shape, octaves=7, first_array=first_array, weights=weights)
+ # more slops
+ data *= data
+ # normalize the height
+ data -= data.min()
+ data = data * ((summit + under_water) / data.max()) - under_water
+ return data
+
+
+def createRgbaMaskImage(mask, color):
+ """Generate an RGBA image where a custom color is apply to the location of
+ the mask. Non masked part of the image is transparent."""
+ image = numpy.zeros((mask.shape[0], mask.shape[1], 4), dtype=numpy.uint8)
+ color = numpy.array(color)
+ image[mask == True] = color
+ return image
+
+
+class FindContours(qt.QMainWindow):
+ """
+ This window show an example of use of a Hdf5TreeView.
+
+ The tree is initialized with a list of filenames. A panel allow to play
+ with internal property configuration of the widget, and a text screen
+ allow to display events.
+ """
+
+ def __init__(self, filenames=None):
+ """
+ :param files_: List of HDF5 or Spec files (pathes or
+ :class:`silx.io.spech5.SpecH5` or :class:`h5py.File`
+ instances)
+ """
+ qt.QMainWindow.__init__(self)
+ self.setWindowTitle("Silx HDF5 widget example")
+
+ self.__plot = silx.gui.plot.Plot2D(parent=self)
+ dummy = numpy.array([[0]])
+ self.__plot.addImage(dummy, legend="image", z=-10, replace=False)
+ dummy = numpy.array([[[0, 0, 0, 0]]])
+ self.__plot.addImage(dummy, legend="iso-pixels", z=0, replace=False)
+
+ self.__algo = None
+ self.__polygons = []
+ self.__customPolygons = []
+ self.__image = None
+ self.__mask = None
+ self.__customValue = None
+
+ mainPanel = qt.QWidget(self)
+ layout = qt.QHBoxLayout()
+ layout.addWidget(self.__createConfigurationPanel(self))
+ layout.addWidget(self.__plot)
+ mainPanel.setLayout(layout)
+
+ self.setCentralWidget(mainPanel)
+
+ def __createConfigurationPanel(self, parent):
+ panel = qt.QWidget(parent=parent)
+ layout = qt.QVBoxLayout()
+ panel.setLayout(layout)
+
+ self.__kind = qt.QButtonGroup(self)
+ self.__kind.setExclusive(True)
+
+ group = qt.QGroupBox(self)
+ group.setTitle("Image")
+ layout.addWidget(group)
+ groupLayout = qt.QVBoxLayout(group)
+
+ button = qt.QRadioButton(parent=panel)
+ button.setText("Island")
+ button.clicked.connect(self.generateIsland)
+ button.setCheckable(True)
+ button.setChecked(True)
+ groupLayout.addWidget(button)
+ self.__kind.addButton(button)
+
+ button = qt.QRadioButton(parent=panel)
+ button.setText("Gravity")
+ button.clicked.connect(self.generateGravityField)
+ button.setCheckable(True)
+ groupLayout.addWidget(button)
+ self.__kind.addButton(button)
+
+ button = qt.QRadioButton(parent=panel)
+ button.setText("Magnetic")
+ button.clicked.connect(self.generateMagneticField)
+ button.setCheckable(True)
+ groupLayout.addWidget(button)
+ self.__kind.addButton(button)
+
+ button = qt.QRadioButton(parent=panel)
+ button.setText("Spiral")
+ button.clicked.connect(self.generateSpiral)
+ button.setCheckable(True)
+ groupLayout.addWidget(button)
+ self.__kind.addButton(button)
+
+ button = qt.QRadioButton(parent=panel)
+ button.setText("Gradient")
+ button.clicked.connect(self.generateGradient)
+ button.setCheckable(True)
+ groupLayout.addWidget(button)
+ self.__kind.addButton(button)
+
+ button = qt.QRadioButton(parent=panel)
+ button.setText("Composite gradient")
+ button.clicked.connect(self.generateCompositeGradient)
+ button.setCheckable(True)
+ groupLayout.addWidget(button)
+ self.__kind.addButton(button)
+
+ button = qt.QPushButton(parent=panel)
+ button.setText("Generate a new image")
+ button.clicked.connect(self.generate)
+ groupLayout.addWidget(button)
+
+ # Contours
+
+ group = qt.QGroupBox(self)
+ group.setTitle("Contours")
+ layout.addWidget(group)
+ groupLayout = qt.QVBoxLayout(group)
+
+ button = qt.QCheckBox(parent=panel)
+ button.setText("Use the plot's mask")
+ button.setCheckable(True)
+ button.setChecked(True)
+ button.clicked.connect(self.updateContours)
+ groupLayout.addWidget(button)
+ self.__useMaskButton = button
+
+ button = qt.QPushButton(parent=panel)
+ button.setText("Update contours")
+ button.clicked.connect(self.updateContours)
+ groupLayout.addWidget(button)
+
+ # Implementations
+
+ group = qt.QGroupBox(self)
+ group.setTitle("Implementation")
+ layout.addWidget(group)
+ groupLayout = qt.QVBoxLayout(group)
+
+ self.__impl = qt.QButtonGroup(self)
+ self.__impl.setExclusive(True)
+
+ button = qt.QRadioButton(parent=panel)
+ button.setText("silx")
+ button.clicked.connect(self.updateContours)
+ button.setCheckable(True)
+ button.setChecked(True)
+ groupLayout.addWidget(button)
+ self.__implMerge = button
+ self.__impl.addButton(button)
+
+ button = qt.QRadioButton(parent=panel)
+ button.setText("silx with cache")
+ button.clicked.connect(self.updateContours)
+ button.setCheckable(True)
+ groupLayout.addWidget(button)
+ self.__implMergeCache = button
+ self.__impl.addButton(button)
+
+ button = qt.QRadioButton(parent=panel)
+ button.setText("skimage")
+ button.clicked.connect(self.updateContours)
+ button.setCheckable(True)
+ groupLayout.addWidget(button)
+ self.__implSkimage = button
+ self.__impl.addButton(button)
+ if MarchingSquaresSciKitImage is None:
+ button.setEnabled(False)
+ button.setToolTip("skimage is not installed or not compatible")
+
+ # Processing
+
+ group = qt.QGroupBox(self)
+ group.setTitle("Processing")
+ layout.addWidget(group)
+ group.setLayout(self.__createInfoLayout(group))
+
+ # Processing
+
+ group = qt.QGroupBox(self)
+ group.setTitle("Custom level")
+ layout.addWidget(group)
+ groupLayout = qt.QVBoxLayout(group)
+
+ label = qt.QLabel(parent=panel)
+ self.__value = qt.QSlider(panel)
+ self.__value.setOrientation(qt.Qt.Horizontal)
+ self.__value.sliderMoved.connect(self.__updateCustomContours)
+ self.__value.valueChanged.connect(self.__updateCustomContours)
+ groupLayout.addWidget(self.__value)
+
+ return panel
+
+ def __createInfoLayout(self, parent):
+ layout = qt.QGridLayout()
+
+ header = qt.QLabel(parent=parent)
+ header.setText("Time: ")
+ label = qt.QLabel(parent=parent)
+ label.setText("")
+ layout.addWidget(header, 0, 0)
+ layout.addWidget(label, 0, 1)
+ self.__timeLabel = label
+
+ header = qt.QLabel(parent=parent)
+ header.setText("Nb polygons: ")
+ label = qt.QLabel(parent=parent)
+ label.setText("")
+ layout.addWidget(header, 2, 0)
+ layout.addWidget(label, 2, 1)
+ self.__polygonsLabel = label
+
+ header = qt.QLabel(parent=parent)
+ header.setText("Nb points: ")
+ label = qt.QLabel(parent=parent)
+ label.setText("")
+ layout.addWidget(header, 1, 0)
+ layout.addWidget(label, 1, 1)
+ self.__pointsLabel = label
+
+ return layout
+
+ def __cleanCustomContour(self):
+ for name in self.__customPolygons:
+ self.__plot.removeCurve(name)
+ self.__customPolygons = []
+ dummy = numpy.array([[[0, 0, 0, 0]]])
+ item = self.__plot.getImage(legend="iso-pixels")
+ item.setData([[[0, 0, 0, 0]]])
+
+ def __cleanPolygons(self):
+ for name in self.__polygons:
+ self.__plot.removeCurve(name)
+
+ def clean(self):
+ self.__cleanCustomContour()
+ self.__cleanPolygons()
+ self.__polygons = []
+ self.__image = None
+ self.__mask = None
+
+ def updateContours(self):
+ self.__redrawContours()
+ self.updateCustomContours()
+
+ def __updateCustomContours(self, value):
+ self.__customValue = value
+ self.updateCustomContours()
+
+ def updateCustomContours(self):
+ if self.__algo is None:
+ return
+ value = self.__customValue
+ self.__cleanCustomContour()
+ if value is None:
+ return
+
+ # iso pixels
+ iso_pixels = self.__algo.find_pixels(value)
+ if len(iso_pixels) != 0:
+ mask = numpy.zeros(self.__image.shape, dtype=numpy.int8)
+ indexes = iso_pixels[:, 0] * self.__image.shape[1] + iso_pixels[:, 1]
+ mask = mask.ravel()
+ mask[indexes] = 1
+ mask.shape = self.__image.shape
+ mask = createRgbaMaskImage(mask, color=numpy.array([255, 0, 0, 128]))
+ item = self.__plot.getImage(legend="iso-pixels")
+ item.setData(mask)
+
+ # iso contours
+ polygons = self.__algo.find_contours(value)
+ for ipolygon, polygon in enumerate(polygons):
+ if len(polygon) == 0:
+ continue
+ isClosed = numpy.allclose(polygon[0], polygon[-1])
+ x = polygon[:, 1] + 0.5
+ y = polygon[:, 0] + 0.5
+ legend = "custom-polygon-%d" % ipolygon
+ self.__customPolygons.append(legend)
+ self.__plot.addCurve(x=x, y=y, linestyle="--", color="red", linewidth=2.0, legend=legend, resetzoom=False)
+
+ def __updateAlgo(self, image, mask=None):
+ if mask is None:
+ if self.__useMaskButton.isChecked():
+ mask = self.__plot.getMaskToolsDockWidget().getSelectionMask()
+
+ self.__image = image
+ self.__mask = mask
+
+ implButton = self.__impl.checkedButton()
+ if implButton == self.__implMerge:
+ from silx.image.marchingsquares import MarchingSquaresMergeImpl
+ self.__algo = MarchingSquaresMergeImpl(self.__image, self.__mask)
+ elif implButton == self.__implMergeCache:
+ from silx.image.marchingsquares import MarchingSquaresMergeImpl
+ self.__algo = MarchingSquaresMergeImpl(self.__image, self.__mask, use_minmax_cache=True)
+ elif implButton == self.__implSkimage and MarchingSquaresSciKitImage is not None:
+ self.__algo = MarchingSquaresSciKitImage(self.__image, self.__mask)
+ else:
+ _logger.error("No algorithm available")
+ self.__algo = None
+
+ def setData(self, image, mask=None, value=0.0):
+ self.clean()
+
+ self.__updateAlgo(image, mask=None)
+
+ # image
+ item = self.__plot.getImage(legend="image")
+ item.setData(image)
+ item.setColormap(self.__colormap)
+
+ self.__plot.resetZoom()
+
+ def __redrawContours(self):
+ self.__updateAlgo(self.__image)
+ if self.__algo is None:
+ return
+ self.__cleanPolygons()
+ self.__drawContours(self.__values, self.__lineStyleCallback)
+
+ def __drawContours(self, values, lineStyleCallback=None):
+ if self.__algo is None:
+ return
+
+ self.__values = values
+ self.__lineStyleCallback = lineStyleCallback
+ if self.__values is None:
+ return
+
+ nbTime = 0
+ nbPolygons = 0
+ nbPoints = 0
+
+ # iso contours
+ ipolygon = 0
+ for ivalue, value in enumerate(values):
+ startTime = time.time()
+ polygons = self.__algo.find_contours(value)
+ nbTime += (time.time() - startTime)
+ nbPolygons += len(polygons)
+ for polygon in polygons:
+ if len(polygon) == 0:
+ continue
+ nbPoints += len(polygon)
+ isClosed = numpy.allclose(polygon[0], polygon[-1])
+ x = polygon[:, 1] + 0.5
+ y = polygon[:, 0] + 0.5
+ legend = "polygon-%d" % ipolygon
+ if lineStyleCallback is not None:
+ extraStyle = lineStyleCallback(value, ivalue, ipolygon)
+ else:
+ extraStyle = {"linestyle": "-", "linewidth": 1.0, "color": "black"}
+ self.__polygons.append(legend)
+ self.__plot.addCurve(x=x, y=y, legend=legend, resetzoom=False, **extraStyle)
+ ipolygon += 1
+
+ self.__timeLabel.setText("%0.3fs" % nbTime)
+ self.__polygonsLabel.setText("%d" % nbPolygons)
+ self.__pointsLabel.setText("%d" % nbPoints)
+
+ def __defineDefaultValues(self, value=None):
+ # Do not use min and max to avoid to create iso contours on small
+ # and many artefacts
+ if value is None:
+ value = self.__image.mean()
+ self.__customValue = value
+ div = 12
+ delta = (self.__image.max() - self.__image.min()) / div
+ self.__value.setValue(value)
+ self.__value.setRange(self.__image.min() + delta,
+ self.__image.min() + delta * (div - 1))
+ self.updateCustomContours()
+
+ def generate(self):
+ self.__kind.checkedButton().click()
+
+ def generateSpiral(self):
+ shape = 512
+ nb_spiral = numpy.random.randint(1, 8)
+ freq = numpy.random.randint(2, 50)
+ image = create_spiral(shape, nb_spiral, freq)
+ image *= 1000.0
+ self.__colormap = Colormap("cool")
+ self.setData(image=image, mask=None)
+ self.__defineDefaultValues()
+
+ def generateIsland(self):
+ shape = (512, 512)
+ image = create_island(shape, summit=4808.72, under_water=1500)
+ self.__colormap = Colormap("terrain")
+ self.setData(image=image, mask=None)
+
+ values = range(-800, 5000, 200)
+
+ def styleCallback(value, ivalue, ipolygon):
+ if value == 0:
+ style = {"linestyle": "-", "linewidth": 1.0, "color": "black"}
+ elif value % 1000 == 0:
+ style = {"linestyle": "--", "linewidth": 0.5, "color": "black"}
+ else:
+ style = {"linestyle": "--", "linewidth": 0.1, "color": "black"}
+ return style
+
+ self.__drawContours(values, styleCallback)
+
+ self.__value.setValue(0)
+ self.__value.setRange(0, 5000)
+ self.__updateCustomContours(0)
+
+ def generateMagneticField(self):
+ shape = 512
+ x1 = numpy.random.random() * 2 - 1
+ y1 = numpy.random.random() * 2 - 1
+ x2 = numpy.random.random() * 2 - 1
+ y2 = numpy.random.random() * 2 - 1
+ image = create_magnetic_field(shape, x1, y1, x2, y2)
+ self.__colormap = Colormap("coolwarm")
+ self.setData(image=image, mask=None)
+
+ maximum = abs(image.max())
+ m = abs(image.min())
+ if m > maximum:
+ maximum = m
+ maximum = int(maximum)
+ values = range(-maximum, maximum, maximum // 20)
+
+ def styleCallback(value, ivalue, ipolygon):
+ if (ivalue % 2) == 0:
+ style = {"linestyle": "-", "linewidth": 0.5, "color": "black"}
+ else:
+ style = {"linestyle": "-", "linewidth": 0.5, "color": "white"}
+ return style
+
+ self.__drawContours(values, styleCallback)
+ self.__defineDefaultValues(value=0)
+
+ def generateGravityField(self):
+ shape = 512
+ nb = numpy.random.randint(2, 10)
+ objects = []
+ for _ in range(nb):
+ x = numpy.random.random() * 2 - 1
+ y = numpy.random.random() * 2 - 1
+ m = numpy.random.random() * 10 + 1.0
+ objects.append((x, y, m))
+ image = create_gravity_field(shape, objects)
+ self.__colormap = Colormap("inferno")
+ self.setData(image=image, mask=None)
+
+ delta = (image.max() - image.min()) / 30.0
+ values = numpy.arange(image.min(), image.max(), delta)
+
+ def styleCallback(value, ivalue, ipolygon):
+ return {"linestyle": "-", "linewidth": 0.1, "color": "white"}
+
+ self.__drawContours(values, styleCallback)
+ self.__defineDefaultValues()
+
+ def generateGradient(self):
+ shape = 512
+ dx = numpy.random.random() * 2 - 1
+ dy = numpy.random.random() * 2 - 1
+ sx = numpy.random.randint(10, 5000) / 10.0
+ sy = numpy.random.randint(10, 5000) / 10.0
+ image = create_gradient(shape, dx=dx, dy=dy, sx=sx, sy=sy)
+ image *= 1000.0
+
+ def styleCallback(value, ivalue, ipolygon):
+ colors = ["#9400D3", "#4B0082", "#0000FF", "#00FF00", "#FFFF00", "#FF7F00", "#FF0000"]
+ color = colors[ivalue % len(colors)]
+ style = {"linestyle": "-", "linewidth": 2.0, "color": color}
+ return style
+ delta = (image.max() - image.min()) / 9.0
+ values = numpy.arange(image.min(), image.max(), delta)
+ values = values[1:8]
+
+ self.__colormap = Colormap("Greys")
+ self.setData(image=image, mask=None)
+ self.__drawContours(values, styleCallback)
+ self.__defineDefaultValues()
+
+ def generateCompositeGradient(self):
+ shape = 512
+ hole = 1 / 4.0
+ dx = numpy.random.random() * hole - hole / 2.0
+ dy = numpy.random.random() * hole - hole * 2
+ sx = numpy.random.random() * 10.0 + 1
+ sy = numpy.random.random() * 10.0 + 1
+ image = create_composite_gradient(shape, dx, dy, sx, sy)
+ image *= 1000.0
+
+ def styleCallback(value, ivalue, ipolygon):
+ colors = ["#9400D3", "#4B0082", "#0000FF", "#00FF00", "#FFFF00", "#FF7F00", "#FF0000"]
+ color = colors[ivalue % len(colors)]
+ style = {"linestyle": "-", "linewidth": 2.0, "color": color}
+ return style
+ delta = (image.max() - image.min()) / 9.0
+ values = numpy.arange(image.min(), image.max(), delta)
+ values = values[1:8]
+
+ self.__colormap = Colormap("Greys")
+ self.setData(image=image, mask=None)
+ self.__drawContours(values, styleCallback)
+ self.__defineDefaultValues()
+
+
+def main():
+ app = qt.QApplication([])
+ sys.excepthook = qt.exceptionHandler
+ window = FindContours()
+ window.generateIsland()
+ window.show()
+ result = app.exec_()
+ # remove ending warnings relative to QTimer
+ app.deleteLater()
+ return result
+
+
+if __name__ == "__main__":
+ result = main()
+ sys.exit(result)
diff --git a/examples/hdf5widget.py b/examples/hdf5widget.py
index 7a8e7ae..bf92d4e 100755
--- a/examples/hdf5widget.py
+++ b/examples/hdf5widget.py
@@ -421,6 +421,16 @@ def get_hdf5_with_nxdata():
gd1.create_dataset("hypercube",
data=numpy.arange(2*3*4*5*6).reshape((2, 3, 4, 5, 6)))
+ gd2 = g.create_group("3D_nonlinear_scaling")
+ gd2.attrs["NX_class"] = u"NXdata"
+ gd2.attrs["signal"] = u"cube"
+ gd2.attrs["axes"] = str_attrs(["img_idx", "rows_coordinates", "cols_coordinates"])
+ gd2.create_dataset("cube", data=numpy.arange(4*5*6).reshape((4, 5, 6)))
+ gd2.create_dataset("img_idx", data=numpy.array([2., -0.1, 8, 3.14]))
+ gd2.create_dataset("rows_coordinates", data=0.1*numpy.arange(5))
+ gd2.create_dataset("cols_coordinates", data=[0.1, 0.6, 0.7, 8., 8.1, 8.2])
+
+
# invalid NXdata
g = h5.create_group("invalid")
g0 = g.create_group("invalid NXdata")
diff --git a/examples/imageview.py b/examples/imageview.py
index c4f80bd..37d1857 100755
--- a/examples/imageview.py
+++ b/examples/imageview.py
@@ -49,6 +49,7 @@ from silx.gui.plot.ImageView import ImageViewMainWindow
from silx.gui import qt
import numpy
+logging.basicConfig()
logger = logging.getLogger(__name__)
diff --git a/examples/plot3dSceneWindow.py b/examples/plot3dSceneWindow.py
index efffcd3..3b78109 100644
--- a/examples/plot3dSceneWindow.py
+++ b/examples/plot3dSceneWindow.py
@@ -45,6 +45,8 @@ __authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "17/11/2017"
+
+import sys
import numpy
from silx.gui import qt
@@ -193,5 +195,8 @@ sceneWidget.addItem(group) # Add the group as an item of the scene
# Show the SceneWidget widget
window.show()
+# Display exception in a pop-up message box
+sys.excepthook = qt.exceptionHandler
+
# Run Qt event loop
qapp.exec_()
diff --git a/examples/plotGL32.py b/examples/plotGL32.py
new file mode 100644
index 0000000..474fbd2
--- /dev/null
+++ b/examples/plotGL32.py
@@ -0,0 +1,46 @@
+import datetime as dt
+import numpy as np
+
+from PyQt5.QtWidgets import QApplication
+from silx.gui.plot import Plot1D
+
+if 1:
+ BACKEND = 'gl' # this gives incorrect results
+else:
+ BACKEND = 'mpl' # this works
+
+def makePlot1D():
+
+ # Make large value with a relatively small range by creating POSIX time stamps.
+ base = dt.datetime.today()
+ dates = [base - dt.timedelta(seconds=x) for x in range(0, 2500, 20)]
+
+ x = np.array([d.timestamp() for d in dates], dtype=np.float64)
+ np.random.seed(seed=1)
+ y = np.random.random(x.shape) * 12 - 3
+
+ print('x range', np.nanmin(x), np.nanmax(x))
+ print('y range', np.nanmin(y), np.nanmax(y))
+
+ plot1D = Plot1D(backend=BACKEND)
+ xAxis = plot1D.getXAxis()
+
+ curve = plot1D.addCurve(x=x, y=y, legend='curve', symbol='o', fill=True)
+
+ plot1D.addMarker(x=x[0], y=y[0], legend='marker', text='the marker', draggable=True)
+ plot1D.addYMarker(y[0], legend='hmarker', text='the H marker', draggable=True)
+ plot1D.addXMarker(x[0], legend='vmarker', text='the V marker', draggable=True)
+
+ plot1D.show()
+
+ return plot1D
+
+
+def main():
+ global app, plot
+ app = QApplication([])
+ plot = makePlot1D()
+ app.exec_()
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/plotInteractiveImageROI.py b/examples/plotInteractiveImageROI.py
new file mode 100644
index 0000000..6c5bc8d
--- /dev/null
+++ b/examples/plotInteractiveImageROI.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""
+This script illustrates image ROI selection in a :class:`~silx.gui.plot.PlotWidget`
+
+It uses :class:`~silx.gui.plot.tools.roi.RegionOfInterestManager` and
+:class:`~silx.gui.plot.tools.roi.RegionOfInterestTableWidget` to handle the
+interactive ROI selection and to display the list of ROIs.
+"""
+
+import sys
+import numpy
+
+from silx.gui import qt
+from silx.gui.plot import Plot2D
+from silx.gui.plot.tools.roi import RegionOfInterestManager
+from silx.gui.plot.tools.roi import RegionOfInterestTableWidget
+from silx.gui.plot.items.roi import RectangleROI
+
+
+def dummy_image():
+ """Create a dummy image"""
+ x = numpy.linspace(-1.5, 1.5, 1024)
+ xv, yv = numpy.meshgrid(x, x)
+ signal = numpy.exp(- (xv ** 2 / 0.15 ** 2
+ + yv ** 2 / 0.25 ** 2))
+ # add noise
+ signal += 0.3 * numpy.random.random(size=signal.shape)
+ return signal
+
+
+app = qt.QApplication([]) # Start QApplication
+
+# Create the plot widget and add an image
+plot = Plot2D()
+plot.getDefaultColormap().setName('viridis')
+plot.addImage(dummy_image())
+
+# Create the object controlling the ROIs and set it up
+roiManager = RegionOfInterestManager(plot)
+roiManager.setColor('pink') # Set the color of ROI
+
+
+# Set the name of each created region of interest
+def updateAddedRegionOfInterest(roi):
+ """Called for each added region of interest: set the name"""
+ if roi.getLabel() == '':
+ roi.setLabel('ROI %d' % len(roiManager.getRois()))
+
+
+roiManager.sigRoiAdded.connect(updateAddedRegionOfInterest)
+
+# Add a rectangular region of interest
+roi = RectangleROI()
+roi.setGeometry(origin=(50, 50), size=(200, 200))
+roi.setLabel('Initial ROI')
+roiManager.addRoi(roi)
+
+# Create the table widget displaying
+roiTable = RegionOfInterestTableWidget()
+roiTable.setRegionOfInterestManager(roiManager)
+
+# Create a toolbar containing buttons for all ROI 'drawing' modes
+roiToolbar = qt.QToolBar() # The layout to store the buttons
+roiToolbar.setIconSize(qt.QSize(16, 16))
+
+for roiClass in roiManager.getSupportedRoiClasses():
+ # Create a tool button and associate it with the QAction of each mode
+ action = roiManager.getInteractionModeAction(roiClass)
+ roiToolbar.addAction(action)
+
+# Add the region of interest table and the buttons to a dock widget
+widget = qt.QWidget()
+layout = qt.QVBoxLayout()
+widget.setLayout(layout)
+layout.addWidget(roiToolbar)
+layout.addWidget(roiTable)
+
+dock = qt.QDockWidget('Image ROI')
+dock.setWidget(widget)
+plot.addTabbedDockWidget(dock)
+
+# Show the widget and start the application
+plot.show()
+result = app.exec_()
+app.deleteLater()
+sys.exit(result)
diff --git a/examples/plotStats.py b/examples/plotStats.py
new file mode 100644
index 0000000..91a444e
--- /dev/null
+++ b/examples/plotStats.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This script is a simple example of how to add your own statistic to a
+:class:`~silx.gui.plot.statsWidget.StatsWidget` from customs
+:class:`~silx.gui.plot.stats.Stats` and display it.
+
+On this example we will:
+
+ - show sum of values for each type
+ - compute curve integrals (only for 'curve').
+ - compute center of mass for all possible items
+
+.. note:: for now the possible types manged by the Stats are ('curve', 'image',
+ 'scatter' and 'histogram')
+"""
+
+__authors__ = ["H. Payno"]
+__license__ = "MIT"
+__date__ = "06/06/2018"
+
+
+from silx.gui import qt
+from silx.gui.plot import Plot1D
+from silx.gui.plot.stats.stats import StatBase
+import numpy
+
+
+class Integral(StatBase):
+ """
+ Simple calculation of the line integral
+ """
+ def __init__(self):
+ StatBase.__init__(self, name='integral', compatibleKinds=('curve',))
+
+ def calculate(self, context):
+ xData, yData = context.data
+ return numpy.trapz(x=xData, y=yData)
+
+
+class COM(StatBase):
+ """
+ Compute data center of mass
+ """
+ def __init__(self):
+ StatBase.__init__(self, name='COM', description="Center of mass")
+
+ def calculate(self, context):
+ if context.kind in ('curve', 'histogram'):
+ xData, yData = context.data
+ deno = numpy.sum(yData).astype(numpy.float32)
+ if deno == 0.0:
+ return 0.0
+ else:
+ return numpy.sum(xData * yData).astype(numpy.float32) / deno
+ elif context.kind == 'scatter':
+ xData, yData, values = context.data
+ values = values.astype(numpy.float64)
+ deno = numpy.sum(values)
+ if deno == 0.0:
+ return float('inf'), float('inf')
+ else:
+ comX = numpy.sum(xData * values) / deno
+ comY = numpy.sum(yData * values) / deno
+ return comX, comY
+
+
+def main():
+ app = qt.QApplication([])
+
+ plot = Plot1D()
+
+ x = numpy.arange(21)
+ y = numpy.arange(21)
+ plot.addCurve(x=x, y=y, legend='myCurve')
+ plot.addCurve(x=x, y=(y + 5), legend='myCurve2')
+
+ plot.setActiveCurve('myCurve')
+
+ plot.addScatter(x=[0, 2, 5, 5, 12, 20],
+ y=[2, 3, 4, 20, 15, 6],
+ value=[5, 6, 7, 10, 90, 20],
+ legend='myScatter')
+
+ stats = [
+ ('sum', numpy.sum),
+ Integral(),
+ (COM(), '{0:.2f}'),
+ ]
+
+ plot.getStatsWidget().setStats(stats)
+ plot.getStatsWidget().parent().setVisible(True)
+ # Update the checkedbox cause we arre playing with the visibility
+ plot.getStatsAction().setChecked(True)
+
+ plot.show()
+ app.exec_()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/examples/plotTimeSeries.py b/examples/plotTimeSeries.py
new file mode 100644
index 0000000..28100fe
--- /dev/null
+++ b/examples/plotTimeSeries.py
@@ -0,0 +1,33 @@
+import datetime as dt
+import numpy as np
+
+from silx.gui import qt
+from silx.gui.plot import Plot1D, TickMode
+
+
+app = qt.QApplication([])
+
+base = dt.datetime.today()
+dates = [base - dt.timedelta(days=x) for x in range(0, 50)]
+
+x = np.array([d.timestamp() for d in dates], dtype=np.uint64)
+
+np.random.seed(seed=1)
+y = np.random.random(x.shape) * 12 - 3
+
+plot1D = Plot1D(backend='gl')
+xAxis = plot1D.getXAxis()
+xAxis.setTickMode(TickMode.TIME_SERIES)
+xAxis.setTimeZone('UTC')
+
+def later():
+ xAxis.setTimeZone(dt.timezone(dt.timedelta(hours=12)))
+ print('set time zone', xAxis.getTimeZone())
+
+qt.QTimer.singleShot(4000, later)
+
+curve = plot1D.addCurve(x=x, y=y, legend='curve')
+plot1D.show()
+
+app.exec_()
+
diff --git a/examples/plotUpdateFromThread.py b/examples/plotUpdateCurveFromThread.py
index 36df63f..a36e5ee 100644
--- a/examples/plotUpdateFromThread.py
+++ b/examples/plotUpdateCurveFromThread.py
@@ -25,14 +25,13 @@
"""This script illustrates the update of a :mod:`silx.gui.plot` widget from a thread.
The problem is that plot and GUI methods should be called from the main thread.
-To safely update the plot from another thread, one need to make the update
-asynchronously from the main thread.
-In this example, this is achieved through a Qt signal.
-
-In this example we create a subclass of :class:`~silx.gui.plot.PlotWindow.Plot1D`
-that adds a thread-safe method to add curves:
-:meth:`ThreadSafePlot1D.addCurveThreadSafe`.
-This thread-safe method is then called from a thread to update the plot.
+To safely update the plot from another thread, one need to execute the update
+asynchronously in the main thread.
+In this example, this is achieved with
+:func:`~silx.gui.utils.concurrent.submitToQtMainThread`.
+
+In this example a thread calls submitToQtMainThread to update the curve
+of a plot.
"""
__authors__ = ["T. Vincent"]
@@ -46,42 +45,15 @@ import time
import numpy
from silx.gui import qt
-from silx.gui.plot import Plot1D
-
-
-class ThreadSafePlot1D(Plot1D):
- """Add a thread-safe :meth:`addCurveThreadSafe` method to Plot1D.
- """
-
- _sigAddCurve = qt.Signal(tuple, dict)
- """Signal used to perform addCurve in the main thread.
-
- It takes args and kwargs as arguments.
- """
+from silx.gui.utils import concurrent
- def __init__(self, parent=None):
- super(ThreadSafePlot1D, self).__init__(parent)
- # Connect the signal to the method actually calling addCurve
- self._sigAddCurve.connect(self.__addCurve)
-
- def __addCurve(self, args, kwargs):
- """Private method calling addCurve from _sigAddCurve"""
- self.addCurve(*args, **kwargs)
-
- def addCurveThreadSafe(self, *args, **kwargs):
- """Thread-safe version of :meth:`silx.gui.plot.Plot.addCurve`
-
- This method takes the same arguments as Plot.addCurve.
-
- WARNING: This method does not return a value as opposed to Plot.addCurve
- """
- self._sigAddCurve.emit(args, kwargs)
+from silx.gui.plot import Plot1D
class UpdateThread(threading.Thread):
- """Thread updating the curve of a :class:`ThreadSafePlot1D`
+ """Thread updating the curve of a :class:`~silx.gui.plot.Plot1D`
- :param plot1d: The ThreadSafePlot1D to update."""
+ :param plot1d: The Plot1D to update."""
def __init__(self, plot1d):
self.plot1d = plot1d
@@ -97,8 +69,12 @@ class UpdateThread(threading.Thread):
"""Method implementing thread loop that updates the plot"""
while self.running:
time.sleep(1)
- self.plot1d.addCurveThreadSafe(
- numpy.arange(1000), numpy.random.random(1000), resetzoom=False)
+ # Run plot update asynchronously
+ concurrent.submitToQtMainThread(
+ self.plot1d.addCurve,
+ numpy.arange(1000),
+ numpy.random.random(1000),
+ resetzoom=False)
def stop(self):
"""Stop the update thread"""
@@ -110,12 +86,12 @@ def main():
global app
app = qt.QApplication([])
- # Create a ThreadSafePlot1D, set its limits and display it
- plot1d = ThreadSafePlot1D()
+ # Create a Plot1D, set its limits and display it
+ plot1d = Plot1D()
plot1d.setLimits(0., 1000., 0., 1.)
plot1d.show()
- # Create the thread that calls ThreadSafePlot1D.addCurveThreadSafe
+ # Create the thread that calls submitToQtMainThread
updateThread = UpdateThread(plot1d)
updateThread.start() # Start updating the plot
diff --git a/examples/plotUpdateImageFromThread.py b/examples/plotUpdateImageFromThread.py
new file mode 100644
index 0000000..5850263
--- /dev/null
+++ b/examples/plotUpdateImageFromThread.py
@@ -0,0 +1,133 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This script illustrates the update of a :class:`~silx.gui.plot.Plot2D`
+widget from a thread.
+
+The problem is that plot and GUI methods should be called from the main thread.
+To safely update the plot from another thread, one need to execute the update
+asynchronously in the main thread.
+In this example, this is achieved with
+:func:`~silx.gui.utils.concurrent.submitToQtMainThread`.
+
+In this example a thread calls submitToQtMainThread to update the image
+of a plot.
+
+Update from 1d to 2d example by Hans Fangohr, European XFEL GmbH, 26 Feb 2018.
+"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "05/09/2017"
+
+
+import threading
+import time
+
+import numpy
+
+from silx.gui import qt
+from silx.gui.utils import concurrent
+from silx.gui.plot import Plot2D
+
+
+Nx = 150
+Ny = 50
+
+
+class UpdateThread(threading.Thread):
+ """Thread updating the image of a :class:`~sil.gui.plot.Plot2D`
+
+ :param plot2d: The Plot2D to update."""
+
+ def __init__(self, plot2d):
+ self.plot2d = plot2d
+ self.running = False
+ self.future_result = None
+ super(UpdateThread, self).__init__()
+
+ def start(self):
+ """Start the update thread"""
+ self.running = True
+ super(UpdateThread, self).start()
+
+ def run(self, pos={'x0': 0, 'y0': 0}):
+ """Method implementing thread loop that updates the plot
+
+ It produces an image every 10 ms or so, and
+ either updates the plot or skip the image
+ """
+ while self.running:
+ time.sleep(0.01)
+
+ # Create image
+ # width of peak
+ sigma_x = 0.15
+ sigma_y = 0.25
+ # x and y positions
+ x = numpy.linspace(-1.5, 1.5, Nx)
+ y = numpy.linspace(-1.0, 1.0, Ny)
+ xv, yv = numpy.meshgrid(x, y)
+ signal = numpy.exp(- ((xv - pos['x0']) ** 2 / sigma_x ** 2
+ + (yv - pos['y0']) ** 2 / sigma_y ** 2))
+ # add noise
+ signal += 0.3 * numpy.random.random(size=signal.shape)
+ # random walk of center of peak ('drift')
+ pos['x0'] += 0.05 * (numpy.random.random() - 0.5)
+ pos['y0'] += 0.05 * (numpy.random.random() - 0.5)
+
+ # If previous frame was not added to the plot yet, skip this one
+ if self.future_result is None or self.future_result.done():
+ # plot the data asynchronously, and
+ # keep a reference to the `future` object
+ self.future_result = concurrent.submitToQtMainThread(
+ self.plot2d.addImage, signal, resetzoom=False)
+
+ def stop(self):
+ """Stop the update thread"""
+ self.running = False
+ self.join(2)
+
+
+def main():
+ global app
+ app = qt.QApplication([])
+
+ # Create a Plot2D, set its limits and display it
+ plot2d = Plot2D()
+ plot2d.setLimits(0, Nx, 0, Ny)
+ plot2d.getDefaultColormap().setVRange(0., 1.5)
+ plot2d.show()
+
+ # Create the thread that calls submitToQtMainThread
+ updateThread = UpdateThread(plot2d)
+ updateThread.start() # Start updating the plot
+
+ app.exec_()
+
+ updateThread.stop() # Stop updating the plot
+
+
+if __name__ == '__main__':
+ main()
diff --git a/examples/plotWidget.py b/examples/plotWidget.py
index 24de519..c8a90a5 100644
--- a/examples/plotWidget.py
+++ b/examples/plotWidget.py
@@ -22,14 +22,15 @@
# THE SOFTWARE.
#
# ###########################################################################*/
-"""This script shows how to subclass :class:`~silx.gui.plot.PlotWidget` to tune its tools.
+"""This script shows how to create a custom window around a PlotWidget.
-It subclasses a :class:`~silx.gui.plot.PlotWidget` and adds toolbars and
-a colorbar by using pluggable widgets:
+It subclasses :class:`QMainWindow`, uses a :class:`~silx.gui.plot.PlotWidget`
+as its central widget and adds toolbars and a colorbar by using pluggable widgets:
+- :class:`~silx.gui.plot.PlotWidget` from :mod:`silx.gui.plot`
+- QToolBar from :mod:`silx.gui.plot.tools`
- QAction from :mod:`silx.gui.plot.actions`
- QToolButton from :mod:`silx.gui.plot.PlotToolButtons`
-- QToolBar from :mod:`silx.gui.plot.PlotTools`
- :class:`silx.gui.plot.ColorBar.ColorBarWidget`
"""
@@ -40,43 +41,25 @@ __date__ = "05/09/2017"
import numpy
from silx.gui import qt
-import silx.gui.plot
+from silx.gui.plot import PlotWidget
+from silx.gui.plot import tools # QToolbars to use with PlotWidget
from silx.gui.plot import actions # QAction to use with PlotWidget
from silx.gui.plot import PlotToolButtons # QToolButton to use with PlotWidget
-from silx.gui.plot.PlotTools import LimitsToolBar
from silx.gui.plot.ColorBar import ColorBarWidget
-class MyPlotWidget(silx.gui.plot.PlotWidget):
- """PlotWidget with an ad hoc toolbar and a colorbar"""
- def __init__(self, parent=None):
- super(MyPlotWidget, self).__init__(parent)
-
- # Add a tool bar to PlotWidget
- toolBar = qt.QToolBar("Plot Tools", self)
- self.addToolBar(toolBar)
-
- # Add actions from silx.gui.plot.action to the toolbar
- resetZoomAction = actions.control.ResetZoomAction(self, self)
- toolBar.addAction(resetZoomAction)
-
- # Add tool buttons from silx.gui.plot.PlotToolButtons
- aspectRatioButton = PlotToolButtons.AspectToolButton(
- parent=self, plot=self)
- toolBar.addWidget(aspectRatioButton)
-
- # Add limits tool bar from silx.gui.plot.PlotTools
- limitsToolBar = LimitsToolBar(parent=self, plot=self)
- self.addToolBar(qt.Qt.BottomToolBarArea, limitsToolBar)
+class MyPlotWindow(qt.QMainWindow):
+ """QMainWindow with selected tools"""
- self._initColorBar()
+ def __init__(self, parent=None):
+ super(MyPlotWindow, self).__init__(parent)
- def _initColorBar(self):
- """Create the ColorBarWidget and add it to the PlotWidget"""
+ # Create a PlotWidget
+ self._plot = PlotWidget(parent=self)
- # Add a colorbar on the right side
- colorBar = ColorBarWidget(parent=self, plot=self)
+ # Create a colorbar linked with the PlotWidget
+ colorBar = ColorBarWidget(parent=self, plot=self._plot)
# Make ColorBarWidget background white by changing its palette
colorBar.setAutoFillBackground(True)
@@ -85,28 +68,67 @@ class MyPlotWidget(silx.gui.plot.PlotWidget):
palette.setColor(qt.QPalette.Window, qt.Qt.white)
colorBar.setPalette(palette)
- # Add the ColorBarWidget by changing PlotWidget's central widget
+ # Combine the ColorBarWidget and the PlotWidget as
+ # this QMainWindow's central widget
gridLayout = qt.QGridLayout()
gridLayout.setSpacing(0)
gridLayout.setContentsMargins(0, 0, 0, 0)
- plot = self.getWidgetHandle() # Get the widget rendering the plot
- gridLayout.addWidget(plot, 0, 0)
+ gridLayout.addWidget(self._plot, 0, 0)
gridLayout.addWidget(colorBar, 0, 1)
gridLayout.setRowStretch(0, 1)
gridLayout.setColumnStretch(0, 1)
- centralWidget = qt.QWidget()
+ centralWidget = qt.QWidget(self)
centralWidget.setLayout(gridLayout)
self.setCentralWidget(centralWidget)
+ # Add ready to use toolbar with zoom and pan interaction mode buttons
+ interactionToolBar = tools.InteractiveModeToolBar(
+ parent=self, plot=self._plot)
+ self.addToolBar(interactionToolBar)
+ # Add toolbar actions to activate keyboard shortcuts
+ self.addActions(interactionToolBar.actions())
+
+ # Add a new toolbar
+ toolBar = qt.QToolBar("Plot Tools", self)
+ self.addToolBar(toolBar)
+
+ # Add actions from silx.gui.plot.action to the toolbar
+ resetZoomAction = actions.control.ResetZoomAction(
+ parent=self, plot=self._plot)
+ toolBar.addAction(resetZoomAction)
+
+ # Add tool buttons from silx.gui.plot.PlotToolButtons
+ aspectRatioButton = PlotToolButtons.AspectToolButton(
+ parent=self, plot=self._plot)
+ toolBar.addWidget(aspectRatioButton)
+
+ # Add ready to use toolbar with copy, save and print buttons
+ outputToolBar = tools.OutputToolBar(parent=self, plot=self._plot)
+ self.addToolBar(outputToolBar)
+ # Add toolbar actions to activate keyboard shortcuts
+ self.addActions(outputToolBar.actions())
+
+ # Add limits tool bar from silx.gui.plot.PlotTools
+ limitsToolBar = tools.LimitsToolBar(parent=self, plot=self._plot)
+ self.addToolBar(qt.Qt.BottomToolBarArea, limitsToolBar)
+
+ def getPlotWidget(self):
+ """Returns the PlotWidget contains in this window"""
+ return self._plot
+
def main():
global app
app = qt.QApplication([])
- # Create the ad hoc plot widget and change its default colormap
- plot = MyPlotWidget()
+ # Create the ad hoc window containing a PlotWidget and associated tools
+ window = MyPlotWindow()
+ window.setAttribute(qt.Qt.WA_DeleteOnClose)
+ window.show()
+
+ # Change the default colormap
+ plot = window.getPlotWidget()
plot.getDefaultColormap().setName('viridis')
- plot.show()
# Add an image to the plot
x = numpy.outer(
diff --git a/examples/scatterMask.py b/examples/scatterMask.py
index 45b1bac..7a407ad 100644
--- a/examples/scatterMask.py
+++ b/examples/scatterMask.py
@@ -1,6 +1,6 @@
# coding: utf-8
# /*##########################################################################
-# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -91,7 +91,7 @@ class MaskScatterWidget(qt.QMainWindow):
:param bool copy: True (default) to get a copy of the mask.
If False, the returned array MUST not be modified.
:return: The array of the mask with dimension of the scatter data.
- If there is no scatter data, an empty array is returned.
+ If there is no scatter data, None is returned.
:rtype: 1D numpy.ndarray of uint8
"""
return self._maskToolsWidget.getSelectionMask(copy=copy)
@@ -109,7 +109,7 @@ class MaskScatterWidget(qt.QMainWindow):
self._plot.addImage(image, legend=self._bgImageLegend,
origin=(xscale[0], yscale[0]),
scale=(xscale[1], yscale[1]),
- z=0, replace=False,
+ z=0,
colormap=colormap)
def setScatter(self, x, y, v=None, info=None, colormap=None):