summaryrefslogtreecommitdiff
path: root/silx/gui/plot/tools
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot/tools')
-rw-r--r--silx/gui/plot/tools/profile/ScatterProfileToolBar.py362
-rw-r--r--silx/gui/plot/tools/roi.py23
-rw-r--r--silx/gui/plot/tools/test/testScatterProfileToolBar.py1
-rw-r--r--silx/gui/plot/tools/test/testTools.py30
-rw-r--r--silx/gui/plot/tools/toolbars.py28
5 files changed, 102 insertions, 342 deletions
diff --git a/silx/gui/plot/tools/profile/ScatterProfileToolBar.py b/silx/gui/plot/tools/profile/ScatterProfileToolBar.py
index fd21515..0d30651 100644
--- a/silx/gui/plot/tools/profile/ScatterProfileToolBar.py
+++ b/silx/gui/plot/tools/profile/ScatterProfileToolBar.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2018 European Synchrotron Radiation Facility
+# Copyright (c) 2018-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,196 +31,18 @@ __date__ = "28/06/2018"
import logging
-import threading
-import time
+import weakref
import numpy
-try:
- from scipy.interpolate import LinearNDInterpolator
-except ImportError:
- LinearNDInterpolator = None
-
- # Fallback using local Delaunay and matplotlib interpolator
- from silx.third_party.scipy_spatial import Delaunay
- import matplotlib.tri
-
from ._BaseProfileToolBar import _BaseProfileToolBar
-from .... import qt
from ... import items
+from ....utils.concurrent import submitToQtMainThread
_logger = logging.getLogger(__name__)
-# TODO support log scale
-
-
-class _InterpolatorInitThread(qt.QThread):
- """Thread building a scatter interpolator
-
- This works in greedy mode in that the signal is only emitted
- when no other request is pending
- """
-
- sigInterpolatorReady = qt.Signal(object)
- """Signal emitted whenever an interpolator is ready
-
- It provides a 3-tuple (points, values, interpolator)
- """
-
- _RUNNING_THREADS_TO_DELETE = []
- """Store reference of no more used threads but still running"""
-
- def __init__(self):
- super(_InterpolatorInitThread, self).__init__()
- self._lock = threading.RLock()
- self._pendingData = None
- self._firstFallbackRun = True
-
- def discard(self, obj=None):
- """Wait for pending thread to complete and delete then
-
- Connect this to the destroyed signal of widget using this thread
- """
- if self.isRunning():
- self.cancel()
- self._RUNNING_THREADS_TO_DELETE.append(self) # Keep a reference
- self.finished.connect(self.__finished)
-
- def __finished(self):
- """Handle finished signal of threads to delete"""
- try:
- self._RUNNING_THREADS_TO_DELETE.remove(self)
- except ValueError:
- _logger.warning('Finished thread no longer in reference list')
-
- def request(self, points, values):
- """Request new initialisation of interpolator
-
- :param numpy.ndarray points: Point coordinates (N, D)
- :param numpy.ndarray values: Values the N points (1D array)
- """
- with self._lock:
- # Possibly replace already pending data
- self._pendingData = points, values
-
- if not self.isRunning():
- self.start()
-
- def cancel(self):
- """Cancel any running/pending requests"""
- with self._lock:
- self._pendingData = 'cancelled'
-
- def run(self):
- """Run the init of the scatter interpolator"""
- if LinearNDInterpolator is None:
- self.run_matplotlib()
- else:
- self.run_scipy()
-
- def run_matplotlib(self):
- """Run the init of the scatter interpolator"""
- if self._firstFallbackRun:
- self._firstFallbackRun = False
- _logger.warning(
- "scipy.spatial.LinearNDInterpolator not available: "
- "Scatter plot interpolator initialisation can freeze the GUI.")
-
- while True:
- with self._lock:
- data = self._pendingData
- self._pendingData = None
-
- if data in (None, 'cancelled'):
- return
-
- points, values = data
-
- startTime = time.time()
- try:
- delaunay = Delaunay(points)
- except:
- _logger.warning(
- "Cannot triangulate scatter data")
- else:
- with self._lock:
- data = self._pendingData
-
- if data is not None: # Break point
- _logger.info('Interpolator discarded after %f s',
- time.time() - startTime)
- else:
-
- x, y = points.T
- triangulation = matplotlib.tri.Triangulation(
- x, y, triangles=delaunay.simplices)
-
- interpolator = matplotlib.tri.LinearTriInterpolator(
- triangulation, values)
-
- with self._lock:
- data = self._pendingData
-
- if data is not None:
- _logger.info('Interpolator discarded after %f s',
- time.time() - startTime)
- else:
- # No other processing requested: emit the signal
- _logger.info("Interpolator initialised in %f s",
- time.time() - startTime)
-
- # Wrap interpolator to have same API as scipy's one
- def wrapper(points):
- return interpolator(*points.T)
-
- self.sigInterpolatorReady.emit(
- (points, values, wrapper))
-
- def run_scipy(self):
- """Run the init of the scatter interpolator"""
- while True:
- with self._lock:
- data = self._pendingData
- self._pendingData = None
-
- if data in (None, 'cancelled'):
- return
-
- points, values = data
-
- startTime = time.time()
- try:
- interpolator = LinearNDInterpolator(points, values)
- except:
- _logger.warning(
- "Cannot initialise scatter profile interpolator")
- else:
- with self._lock:
- data = self._pendingData
-
- if data is not None: # Break point
- _logger.info('Interpolator discarded after %f s',
- time.time() - startTime)
- else:
- # First call takes a while, do it here
- interpolator([(0., 0.)])
-
- with self._lock:
- data = self._pendingData
-
- if data is not None:
- _logger.info('Interpolator discarded after %f s',
- time.time() - startTime)
- else:
- # No other processing requested: emit the signal
- _logger.info("Interpolator initialised in %f s",
- time.time() - startTime)
- self.sigInterpolatorReady.emit(
- (points, values, interpolator))
-
-
class ScatterProfileToolBar(_BaseProfileToolBar):
"""QToolBar providing scatter plot profiling tools
@@ -233,49 +55,13 @@ class ScatterProfileToolBar(_BaseProfileToolBar):
super(ScatterProfileToolBar, self).__init__(parent, plot, title)
self.__nPoints = 1024
- self.__interpolator = None
- self.__interpolatorCache = None # points, values, interpolator
-
- self.__initThread = _InterpolatorInitThread()
- self.destroyed.connect(self.__initThread.discard)
- self.__initThread.sigInterpolatorReady.connect(
- self.__interpolatorReady)
-
- roiManager = self._getRoiManager()
- if roiManager is None:
- _logger.error(
- "Error during scatter profile toolbar initialisation")
- else:
- roiManager.sigInteractiveModeStarted.connect(
- self.__interactionStarted)
- roiManager.sigInteractiveModeFinished.connect(
- self.__interactionFinished)
- if roiManager.isStarted():
- self.__interactionStarted(roiManager.getCurrentInteractionModeRoiClass())
-
- def __interactionStarted(self, roiClass):
- """Handle start of ROI interaction"""
- plot = self.getPlotWidget()
- if plot is None:
- return
-
- plot.sigActiveScatterChanged.connect(self.__activeScatterChanged)
-
- scatter = plot._getActiveItem(kind='scatter')
- legend = None if scatter is None else scatter.getLegend()
- self.__activeScatterChanged(None, legend)
+ self.__scatterRef = None
+ self.__futureInterpolator = None
- def __interactionFinished(self):
- """Handle end of ROI interaction"""
plot = self.getPlotWidget()
- if plot is None:
- return
-
- plot.sigActiveScatterChanged.disconnect(self.__activeScatterChanged)
-
- scatter = plot._getActiveItem(kind='scatter')
- legend = None if scatter is None else scatter.getLegend()
- self.__activeScatterChanged(legend, None)
+ if plot is not None:
+ self._setScatterItem(plot._getActiveItem(kind='scatter'))
+ plot.sigActiveScatterChanged.connect(self.__activeScatterChanged)
def __activeScatterChanged(self, previous, legend):
"""Handle change of active scatter
@@ -283,35 +69,37 @@ class ScatterProfileToolBar(_BaseProfileToolBar):
:param Union[str,None] previous:
:param Union[str,None] legend:
"""
- self.__initThread.cancel()
+ plot = self.getPlotWidget()
+ if plot is None or legend is None:
+ scatter = None
+ else:
+ scatter = plot.getScatter(legend)
+ self._setScatterItem(scatter)
- # Reset interpolator
- self.__interpolator = None
+ def _getScatterItem(self):
+ """Returns the scatter item currently handled by this tool.
- plot = self.getPlotWidget()
- if plot is None:
- _logger.error("Associated PlotWidget no longer exists")
+ :rtype: ~silx.gui.plot.items.Scatter
+ """
+ return None if self.__scatterRef is None else self.__scatterRef()
+ def _setScatterItem(self, scatter):
+ """Set the scatter tracked by this tool
+
+ :param Union[None,silx.gui.plot.items.Scatter] scatter:
+ """
+ self.__futureInterpolator = None # Reset currently expected future
+
+ previousScatter = self._getScatterItem()
+ if previousScatter is not None:
+ previousScatter.sigItemChanged.disconnect(
+ self.__scatterItemChanged)
+
+ if scatter is None:
+ self.__scatterRef = None
else:
- if previous is not None: # Disconnect signal
- scatter = plot.getScatter(previous)
- if scatter is not None:
- scatter.sigItemChanged.disconnect(
- self.__scatterItemChanged)
-
- if legend is not None:
- scatter = plot.getScatter(legend)
- if scatter is None:
- _logger.error("Cannot retrieve active scatter")
-
- else:
- scatter.sigItemChanged.connect(self.__scatterItemChanged)
- points = numpy.transpose(numpy.array((
- scatter.getXData(copy=False),
- scatter.getYData(copy=False))))
- values = scatter.getValueData(copy=False)
-
- self.__updateInterpolator(points, values)
+ self.__scatterRef = weakref.ref(scatter)
+ scatter.sigItemChanged.connect(self.__scatterItemChanged)
# Refresh profile
self.updateProfile()
@@ -322,49 +110,15 @@ class ScatterProfileToolBar(_BaseProfileToolBar):
:param ItemChangedType event:
"""
if event == items.ItemChangedType.DATA:
- self.__interpolator = None
- scatter = self.sender()
- if scatter is None:
- _logger.error("Cannot retrieve updated scatter item")
-
- else:
- points = numpy.transpose(numpy.array((
- scatter.getXData(copy=False),
- scatter.getYData(copy=False))))
- values = scatter.getValueData(copy=False)
-
- self.__updateInterpolator(points, values)
-
- # Handle interpolator init thread
-
- def __updateInterpolator(self, points, values):
- """Update used interpolator with new data"""
- if (self.__interpolatorCache is not None and
- len(points) == len(self.__interpolatorCache[0]) and
- numpy.all(numpy.equal(self.__interpolatorCache[0], points)) and
- numpy.all(numpy.equal(self.__interpolatorCache[1], values))):
- # Reuse previous interpolator
- _logger.info(
- 'Scatter changed: Reuse previous interpolator')
- self.__interpolator = self.__interpolatorCache[2]
-
- else:
- # Interpolator needs update: Start background processing
- _logger.info(
- 'Scatter changed: Rebuild interpolator')
- self.__interpolator = None
- self.__interpolatorCache = None
- self.__initThread.request(points, values)
-
- def __interpolatorReady(self, data):
- """Handle end of init interpolator thread"""
- points, values, interpolator = data
- self.__interpolator = interpolator
- self.__interpolatorCache = None if interpolator is None else data
- self.updateProfile()
+ self.updateProfile() # Refresh profile
def hasPendingOperations(self):
- return self.__initThread.isRunning()
+ """Returns True if waiting for an interpolator to be ready
+
+ :rtype: bool
+ """
+ return (self.__futureInterpolator is not None and
+ not self.__futureInterpolator.done())
# Number of points
@@ -383,8 +137,9 @@ class ScatterProfileToolBar(_BaseProfileToolBar):
npoints = int(npoints)
if npoints < 1:
raise ValueError("Unsupported number of points: %d" % npoints)
- else:
+ elif npoints != self.__nPoints:
self.__nPoints = npoints
+ self.updateProfile()
# Overridden methods
@@ -400,11 +155,16 @@ class ScatterProfileToolBar(_BaseProfileToolBar):
"""
if self.hasPendingOperations():
return 'Pre-processing data...'
-
else:
return super(ScatterProfileToolBar, self).computeProfileTitle(
x0, y0, x1, y1)
+ def __futureDone(self, future):
+ """Handle completion of the interpolator creation"""
+ if future is self.__futureInterpolator:
+ # Only handle future callbacks for the current one
+ submitToQtMainThread(self.updateProfile)
+
def computeProfile(self, x0, y0, x1, y1):
"""Compute corresponding profile
@@ -414,16 +174,32 @@ class ScatterProfileToolBar(_BaseProfileToolBar):
:param float y1: Profile end point Y coord
:return: (points, values) profile data or None
"""
- if self.__interpolator is None:
+ scatter = self._getScatterItem()
+ if scatter is None or self.hasPendingOperations():
return None
- nPoints = self.getNPoints()
+ # Lazy async request of the interpolator
+ future = scatter._getInterpolator()
+ if future is not self.__futureInterpolator:
+ # First time we request this interpolator
+ self.__futureInterpolator = future
+ if not future.done():
+ future.add_done_callback(self.__futureDone)
+ return None
+
+ if future.cancelled() or future.exception() is not None:
+ return None # Something went wrong
+ interpolator = future.result()
+ if interpolator is None:
+ return None # Cannot init an interpolator
+
+ nPoints = self.getNPoints()
points = numpy.transpose((
numpy.linspace(x0, x1, nPoints, endpoint=True),
numpy.linspace(y0, y1, nPoints, endpoint=True)))
- values = self.__interpolator(points)
+ values = interpolator(points)
if not numpy.any(numpy.isfinite(values)):
return None # Profile outside convex hull
diff --git a/silx/gui/plot/tools/roi.py b/silx/gui/plot/tools/roi.py
index 98295ba..eb933a0 100644
--- a/silx/gui/plot/tools/roi.py
+++ b/silx/gui/plot/tools/roi.py
@@ -106,6 +106,9 @@ class RegionOfInterestManager(qt.QObject):
self._rois = [] # List of ROIs
self._drawnROI = None # New ROI being currently drawn
+ # Handle unique selection of interaction mode action
+ self._actionGroup = qt.QActionGroup(self)
+
self._roiClass = None
self._color = rgba('red')
@@ -158,6 +161,8 @@ class RegionOfInterestManager(qt.QObject):
action.setChecked(self.getCurrentInteractionModeRoiClass() is roiClass)
action.setToolTip(text)
+ self._actionGroup.addAction(action)
+
action.triggered[bool].connect(functools.partial(
WeakMethodProxy(self._modeActionTriggered), roiClass=roiClass))
self._modeActions[roiClass] = action
@@ -171,9 +176,6 @@ class RegionOfInterestManager(qt.QObject):
"""
if checked:
self.start(roiClass)
- else: # Keep action checked
- action = self.sender()
- action.setChecked(True)
def _updateModeActions(self):
"""Check/Uncheck action corresponding to current mode"""
@@ -781,9 +783,9 @@ class RegionOfInterestTableWidget(qt.QTableWidget):
super(RegionOfInterestTableWidget, self).__init__(parent)
self._roiManagerRef = None
- self.setColumnCount(5)
- self.setHorizontalHeaderLabels(
- ['Label', 'Edit', 'Kind', 'Coordinates', ''])
+ headers = ['Label', 'Edit', 'Kind', 'Coordinates', '']
+ self.setColumnCount(len(headers))
+ self.setHorizontalHeaderLabels(headers)
horizontalHeader = self.horizontalHeader()
horizontalHeader.setDefaultAlignment(qt.Qt.AlignLeft)
@@ -815,9 +817,10 @@ class RegionOfInterestTableWidget(qt.QTableWidget):
manager = self.getRegionOfInterestManager()
roi = manager.getRois()[index]
else:
- roi = None
+ return
if column == 0:
+ roi.setVisible(item.checkState() == qt.Qt.Checked)
roi.setLabel(item.text())
elif column == 1:
roi.setEditable(
@@ -884,11 +887,13 @@ class RegionOfInterestTableWidget(qt.QTableWidget):
for index, roi in enumerate(rois):
baseFlags = qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled
- # Label
+ # Label and visible
label = roi.getLabel()
item = qt.QTableWidgetItem(label)
- item.setFlags(baseFlags | qt.Qt.ItemIsEditable)
+ item.setFlags(baseFlags | qt.Qt.ItemIsEditable | qt.Qt.ItemIsUserCheckable)
item.setData(qt.Qt.UserRole, index)
+ item.setCheckState(
+ qt.Qt.Checked if roi.isVisible() else qt.Qt.Unchecked)
self.setItem(index, 0, item)
# Editable
diff --git a/silx/gui/plot/tools/test/testScatterProfileToolBar.py b/silx/gui/plot/tools/test/testScatterProfileToolBar.py
index 0f4b668..714746a 100644
--- a/silx/gui/plot/tools/test/testScatterProfileToolBar.py
+++ b/silx/gui/plot/tools/test/testScatterProfileToolBar.py
@@ -101,6 +101,7 @@ class TestScatterProfileToolBar(TestCaseQt, ParametricTestCase):
self.qWait(200)
if not self.profile.hasPendingOperations():
break
+ self.qapp.processEvents()
self.assertIsNotNone(self.profile.getProfileValues())
points = self.profile.getProfilePoints()
diff --git a/silx/gui/plot/tools/test/testTools.py b/silx/gui/plot/tools/test/testTools.py
index f4adda0..70c8105 100644
--- a/silx/gui/plot/tools/test/testTools.py
+++ b/silx/gui/plot/tools/test/testTools.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2016-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
@@ -41,34 +41,6 @@ from silx.gui.plot import tools
from silx.gui.plot.test.utils import PlotWidgetTestCase
-# Makes sure a QApplication exists
-_qapp = qt.QApplication.instance() or qt.QApplication([])
-
-
-def _tearDownDocTest(docTest):
- """Tear down to use for test from docstring.
-
- Checks that plot widget is displayed
- """
- plot = docTest.globs['plot']
- qWaitForWindowExposedAndActivate(plot)
- plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- plot.close()
- del plot
-
-# Disable doctest because of
-# "NameError: name 'numpy' is not defined"
-#
-# import doctest
-# positionInfoTestSuite = doctest.DocTestSuite(
-# PlotTools, tearDown=_tearDownDocTest,
-# optionflags=doctest.ELLIPSIS)
-# """Test suite of tests from PlotTools docstrings.
-#
-# Test PositionInfo and ProfileToolBar docstrings.
-# """
-
-
class TestPositionInfo(PlotWidgetTestCase):
"""Tests for PositionInfo widget."""
diff --git a/silx/gui/plot/tools/toolbars.py b/silx/gui/plot/tools/toolbars.py
index 28fb7f9..04d0cfc 100644
--- a/silx/gui/plot/tools/toolbars.py
+++ b/silx/gui/plot/tools/toolbars.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2018 European Synchrotron Radiation Facility
+# Copyright (c) 2018-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
@@ -34,6 +34,7 @@ from ... import qt
from .. import actions
from ..PlotWidget import PlotWidget
from .. import PlotToolButtons
+from ....utils.deprecation import deprecated
class InteractiveModeToolBar(qt.QToolBar):
@@ -302,9 +303,9 @@ class ScatterToolBar(qt.QToolBar):
parent=self, plot=plot)
self.addAction(self._colormapAction)
- self._symbolToolButton = PlotToolButtons.SymbolToolButton(
- parent=self, plot=plot)
- self.addWidget(self._symbolToolButton)
+ self._visualizationToolButton = \
+ PlotToolButtons.ScatterVisualizationToolButton(parent=self, plot=plot)
+ self.addWidget(self._visualizationToolButton)
def getResetZoomAction(self):
"""Returns the QAction to reset the zoom.
@@ -341,16 +342,21 @@ class ScatterToolBar(qt.QToolBar):
"""
return self._colormapAction
- def getSymbolToolButton(self):
- """Returns the QToolButton controlling symbol size and marker.
-
- :rtype: SymbolToolButton
- """
- return self._symbolToolButton
-
def getKeepDataAspectRatioButton(self):
"""Returns the QToolButton controlling data aspect ratio.
:rtype: QToolButton
"""
return self._keepDataAspectRatioButton
+
+ def getScatterVisualizationToolButton(self):
+ """Returns the QToolButton controlling the visualization mode.
+
+ :rtype: ScatterVisualizationToolButton
+ """
+ return self._visualizationToolButton
+
+ @deprecated(replacement='getScatterVisualizationToolButton',
+ since_version='0.11.0')
+ def getSymbolToolButton(self):
+ return self.getScatterVisualizationToolButton()