diff options
Diffstat (limited to 'silx/gui/plot/test')
-rw-r--r-- | silx/gui/plot/test/__init__.py | 71 | ||||
-rw-r--r-- | silx/gui/plot/test/testAlphaSlider.py | 221 | ||||
-rw-r--r-- | silx/gui/plot/test/testColorBar.py | 240 | ||||
-rw-r--r-- | silx/gui/plot/test/testColormapDialog.py | 68 | ||||
-rw-r--r-- | silx/gui/plot/test/testColors.py | 94 | ||||
-rw-r--r-- | silx/gui/plot/test/testCurvesROIWidget.py | 153 | ||||
-rw-r--r-- | silx/gui/plot/test/testInteraction.py | 89 | ||||
-rw-r--r-- | silx/gui/plot/test/testLegendSelector.py | 143 | ||||
-rw-r--r-- | silx/gui/plot/test/testMaskToolsWidget.py | 295 | ||||
-rw-r--r-- | silx/gui/plot/test/testPlot.py | 633 | ||||
-rw-r--r-- | silx/gui/plot/test/testPlotInteraction.py | 167 | ||||
-rw-r--r-- | silx/gui/plot/test/testPlotTools.py | 203 | ||||
-rw-r--r-- | silx/gui/plot/test/testPlotWidget.py | 967 | ||||
-rw-r--r-- | silx/gui/plot/test/testPlotWindow.py | 138 | ||||
-rw-r--r-- | silx/gui/plot/test/testProfile.py | 183 | ||||
-rw-r--r-- | silx/gui/plot/test/testScatterMaskToolsWidget.py | 313 | ||||
-rw-r--r-- | silx/gui/plot/test/testStackView.py | 209 |
17 files changed, 4187 insertions, 0 deletions
diff --git a/silx/gui/plot/test/__init__.py b/silx/gui/plot/test/__init__.py new file mode 100644 index 0000000..b4378c7 --- /dev/null +++ b/silx/gui/plot/test/__init__.py @@ -0,0 +1,71 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-2017 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. +# +# ###########################################################################*/ +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "18/02/2016" + + +import unittest + +from .._utils.test import suite as testUtilsSuite +from .testColorBar import suite as testColorBarSuite +from .testColormapDialog import suite as testColormapDialogSuite +from .testColors import suite as testColorsSuite +from .testCurvesROIWidget import suite as testCurvesROIWidgetSuite +from .testAlphaSlider import suite as testAlphaSliderSuite +from .testInteraction import suite as testInteractionSuite +from .testLegendSelector import suite as testLegendSelectorSuite +from .testMaskToolsWidget import suite as testMaskToolsWidgetSuite +from .testScatterMaskToolsWidget import suite as testScatterMaskToolsWidgetSuite +from .testPlotInteraction import suite as testPlotInteractionSuite +from .testPlotTools import suite as testPlotToolsSuite +from .testPlotWidget import suite as testPlotWidgetSuite +from .testPlotWindow import suite as testPlotWindowSuite +from .testPlot import suite as testPlotSuite +from .testProfile import suite as testProfileSuite +from .testStackView import suite as testStackViewSuite + + +def suite(): + test_suite = unittest.TestSuite() + test_suite.addTests( + [testUtilsSuite(), + testColorBarSuite(), + testColorsSuite(), + testColormapDialogSuite(), + testCurvesROIWidgetSuite(), + testAlphaSliderSuite(), + testInteractionSuite(), + testLegendSelectorSuite(), + testMaskToolsWidgetSuite(), + testScatterMaskToolsWidgetSuite(), + testPlotInteractionSuite(), + testPlotSuite(), + testPlotToolsSuite(), + testPlotWidgetSuite(), + testPlotWindowSuite(), + testProfileSuite(), + testStackViewSuite()]) + return test_suite diff --git a/silx/gui/plot/test/testAlphaSlider.py b/silx/gui/plot/test/testAlphaSlider.py new file mode 100644 index 0000000..304a562 --- /dev/null +++ b/silx/gui/plot/test/testAlphaSlider.py @@ -0,0 +1,221 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2017 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. +# +# ###########################################################################*/ +"""Tests for ImageAlphaSlider""" + + +__authors__ = ["P. Knobel"] +__license__ = "MIT" +__date__ = "28/03/2017" + +import numpy +import unittest + +from silx.gui import qt +from silx.gui.test.utils import TestCaseQt +from silx.gui.plot import PlotWidget +from silx.gui.plot import AlphaSlider + +# Makes sure a QApplication exists +_qapp = qt.QApplication.instance() or qt.QApplication([]) + + +class TestActiveImageAlphaSlider(TestCaseQt): + def setUp(self): + super(TestActiveImageAlphaSlider, self).setUp() + self.plot = PlotWidget() + self.aslider = AlphaSlider.ActiveImageAlphaSlider(plot=self.plot) + self.aslider.setOrientation(qt.Qt.Horizontal) + + toolbar = qt.QToolBar("plot", self.plot) + toolbar.addWidget(self.aslider) + self.plot.addToolBar(toolbar) + + self.plot.show() + self.qWaitForWindowExposed(self.plot) + + self.mouseMove(self.plot) # Move to center + self.qapp.processEvents() + + def tearDown(self): + self.qapp.processEvents() + self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) + self.plot.close() + del self.plot + del self.aslider + + super(TestActiveImageAlphaSlider, self).tearDown() + + def testWidgetEnabled(self): + # no active image initially, slider must be deactivate + self.assertFalse(self.aslider.isEnabled()) + + self.plot.addImage(numpy.array([[0, 1, 2], [3, 4, 5]])) + # now we have an active image + self.assertTrue(self.aslider.isEnabled()) + + self.plot.setActiveImage(None) + self.assertFalse(self.aslider.isEnabled()) + + def testGetImage(self): + self.plot.addImage(numpy.array([[0, 1, 2], [3, 4, 5]])) + self.assertEqual(self.plot.getActiveImage(), + self.aslider.getItem()) + + self.plot.addImage(numpy.array([[0, 1, 3], [2, 4, 6]]), legend="2") + self.plot.setActiveImage("2") + self.assertEqual(self.plot.getImage("2"), + self.aslider.getItem()) + + def testGetAlpha(self): + self.plot.addImage(numpy.array([[0, 1, 2], [3, 4, 5]]), legend="1") + self.aslider.setValue(137) + self.assertAlmostEqual(self.aslider.getAlpha(), + 137. / 255) + + +class TestNamedImageAlphaSlider(TestCaseQt): + def setUp(self): + super(TestNamedImageAlphaSlider, self).setUp() + self.plot = PlotWidget() + self.aslider = AlphaSlider.NamedImageAlphaSlider(plot=self.plot) + self.aslider.setOrientation(qt.Qt.Horizontal) + + toolbar = qt.QToolBar("plot", self.plot) + toolbar.addWidget(self.aslider) + self.plot.addToolBar(toolbar) + + self.plot.show() + self.qWaitForWindowExposed(self.plot) + + self.mouseMove(self.plot) # Move to center + self.qapp.processEvents() + + def tearDown(self): + self.qapp.processEvents() + self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) + self.plot.close() + del self.plot + del self.aslider + + super(TestNamedImageAlphaSlider, self).tearDown() + + def testWidgetEnabled(self): + # no image set initially, slider must be deactivate + self.assertFalse(self.aslider.isEnabled()) + + self.plot.addImage(numpy.array([[0, 1, 2], [3, 4, 5]]), legend="1") + self.aslider.setLegend("1") + # now we have an image set + self.assertTrue(self.aslider.isEnabled()) + + def testGetImage(self): + self.plot.addImage(numpy.array([[0, 1, 2], [3, 4, 5]]), legend="1") + self.plot.addImage(numpy.array([[0, 1, 3], [2, 4, 6]]), legend="2") + self.aslider.setLegend("1") + self.assertEqual(self.plot.getImage("1"), + self.aslider.getItem()) + + self.aslider.setLegend("2") + self.assertEqual(self.plot.getImage("2"), + self.aslider.getItem()) + + def testGetAlpha(self): + self.plot.addImage(numpy.array([[0, 1, 2], [3, 4, 5]]), legend="1") + self.aslider.setLegend("1") + self.aslider.setValue(128) + self.assertAlmostEqual(self.aslider.getAlpha(), + 128. / 255) + + +class TestNamedScatterAlphaSlider(TestCaseQt): + def setUp(self): + super(TestNamedScatterAlphaSlider, self).setUp() + self.plot = PlotWidget() + self.aslider = AlphaSlider.NamedScatterAlphaSlider(plot=self.plot) + self.aslider.setOrientation(qt.Qt.Horizontal) + + toolbar = qt.QToolBar("plot", self.plot) + toolbar.addWidget(self.aslider) + self.plot.addToolBar(toolbar) + + self.plot.show() + self.qWaitForWindowExposed(self.plot) + + self.mouseMove(self.plot) # Move to center + self.qapp.processEvents() + + def tearDown(self): + self.qapp.processEvents() + self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) + self.plot.close() + del self.plot + del self.aslider + + super(TestNamedScatterAlphaSlider, self).tearDown() + + def testWidgetEnabled(self): + # no Scatter set initially, slider must be deactivate + self.assertFalse(self.aslider.isEnabled()) + + self.plot.addScatter([0, 1, 2], [2, 3, 4], [5, 6, 7], + legend="1") + self.aslider.setLegend("1") + # now we have an image set + self.assertTrue(self.aslider.isEnabled()) + + def testGetScatter(self): + self.plot.addScatter([0, 1, 2], [2, 3, 4], [5, 6, 7], + legend="1") + self.plot.addScatter([0, 10, 20], [20, 30, 40], [50, 60, 70], + legend="2") + self.aslider.setLegend("1") + self.assertEqual(self.plot.getScatter("1"), + self.aslider.getItem()) + + self.aslider.setLegend("2") + self.assertEqual(self.plot.getScatter("2"), + self.aslider.getItem()) + + def testGetAlpha(self): + self.plot.addScatter([0, 10, 20], [20, 30, 40], [50, 60, 70], + legend="1") + self.aslider.setLegend("1") + self.aslider.setValue(128) + self.assertAlmostEqual(self.aslider.getAlpha(), + 128. / 255) + + +def suite(): + test_suite = unittest.TestSuite() + # test_suite.addTest(positionInfoTestSuite) + for testClass in (TestActiveImageAlphaSlider, TestNamedImageAlphaSlider, + TestNamedScatterAlphaSlider): + test_suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase( + testClass)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/gui/plot/test/testColorBar.py b/silx/gui/plot/test/testColorBar.py new file mode 100644 index 0000000..797ff03 --- /dev/null +++ b/silx/gui/plot/test/testColorBar.py @@ -0,0 +1,240 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-2017 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. +# +# ###########################################################################*/ +"""Basic tests for ColorBar featues and sub widgets of Colorbar module""" + +__authors__ = ["H. Payno"] +__license__ = "MIT" +__date__ = "11/04/2017" + +import unittest +from silx.gui.test.utils import TestCaseQt +from silx.gui.plot.ColorBar import _ColorScale +from silx.gui.plot.ColorBar import ColorBarWidget +from silx.gui.plot import Plot2D +import numpy + + +class TestColorScale(unittest.TestCase): + """Test that interaction with the colorScale is correct""" + def setUp(self): + self.colorScaleWidget = _ColorScale(colormap=None, parent=None) + + def tearDown(self): + self.colorScaleWidget.deleteLater() + self.colorScaleWidget = None + + def testRelativePositionLinear(self): + self.colorMapLin1 = { 'name': 'gray', 'normalization': 'linear', + 'autoscale': False, 'vmin': 0.0, 'vmax': 1.0 } + self.colorScaleWidget.setColormap(self.colorMapLin1) + + self.assertTrue( + self.colorScaleWidget.getValueFromRelativePosition(0.25) == 0.25) + self.assertTrue( + self.colorScaleWidget.getValueFromRelativePosition(0.5) == 0.5) + self.assertTrue( + self.colorScaleWidget.getValueFromRelativePosition(1.0) == 1.0) + + self.colorMapLin2 = { 'name': 'viridis', 'normalization': 'linear', + 'autoscale': False, 'vmin': -10, 'vmax': 0 } + self.colorScaleWidget.setColormap(self.colorMapLin2) + + self.assertTrue( + self.colorScaleWidget.getValueFromRelativePosition(0.25) == -7.5) + self.assertTrue( + self.colorScaleWidget.getValueFromRelativePosition(0.5) == -5.0) + self.assertTrue( + self.colorScaleWidget.getValueFromRelativePosition(1.0) == 0.0) + + def testRelativePositionLog(self): + self.colorMapLog1 = { 'name': 'temperature', 'normalization': 'log', + 'autoscale': False, 'vmin': 1.0, 'vmax': 100.0 } + + self.colorScaleWidget.setColormap(self.colorMapLog1) + + val = self.colorScaleWidget.getValueFromRelativePosition(1.0) + self.assertTrue(val == 100.0) + + val = self.colorScaleWidget.getValueFromRelativePosition(0.5) + self.assertTrue(val == 10.0) + + val = self.colorScaleWidget.getValueFromRelativePosition(0.0) + self.assertTrue(val == 1.0) + + def testNegativeLogMin(self): + colormap = { 'name': 'gray', 'normalization': 'log', + 'autoscale': False, 'vmin': -1.0, 'vmax': 1.0 } + + with self.assertRaises(ValueError): + self.colorScaleWidget.setColormap(colormap) + + def testNegativeLogMax(self): + colormap = { 'name': 'gray', 'normalization': 'log', + 'autoscale': False, 'vmin': 1.0, 'vmax': -1.0 } + + with self.assertRaises(ValueError): + self.colorScaleWidget.setColormap(colormap) + +class TestNoAutoscale(unittest.TestCase): + """Test that ticks and color displayed are correct in the case of a colormap + with no autoscale + """ + + def setUp(self): + self.plot = Plot2D() + self.colorBar = ColorBarWidget(parent=None, plot=self.plot) + self.tickBar = self.colorBar.getColorScaleBar().getTickBar() + self.colorScale = self.colorBar.getColorScaleBar().getColorScale() + + def tearDown(self): + self.tickBar = None + self.colorScale = None + del self.colorBar + self.plot.close() + del self.plot + + def testLogNormNoAutoscale(self): + colormapLog = { 'name': 'gray', 'normalization': 'log', + 'autoscale': False, 'vmin': 1.0, 'vmax': 100.0 } + + data = numpy.linspace(10, 1e10, 9).reshape(3, 3) + self.plot.addImage(data=data, colormap=colormapLog, legend='toto') + self.plot.setActiveImage('toto') + + # test Ticks + self.tickBar.setTicksNumber(10) + self.tickBar.computeTicks() + + ticksTh = numpy.linspace(1.0, 100.0, 10) + ticksTh = 10**ticksTh + numpy.array_equal(self.tickBar.ticks, ticksTh) + + # test ColorScale + val = self.colorScale.getValueFromRelativePosition(1.0) + self.assertTrue(val == 100.0) + + val = self.colorScale.getValueFromRelativePosition(0.0) + self.assertTrue(val == 1.0) + + def testLinearNormNoAutoscale(self): + colormapLog = { 'name': 'gray', 'normalization': 'linear', + 'autoscale': False, 'vmin': -4, 'vmax': 5 } + + data = numpy.linspace(1, 9, 9).reshape(3, 3) + self.plot.addImage(data=data, colormap=colormapLog, legend='toto') + self.plot.setActiveImage('toto') + + # test Ticks + self.tickBar.setTicksNumber(10) + self.tickBar.computeTicks() + + numpy.array_equal(self.tickBar.ticks, numpy.linspace(-4, 5, 10)) + + # test ColorScale + val = self.colorScale.getValueFromRelativePosition(1.0) + self.assertTrue(val == 5.0) + + val = self.colorScale.getValueFromRelativePosition(0.0) + self.assertTrue(val == -4.0) + +class TestColorbarWidget(TestCaseQt): + """Test interaction with the ColorScaleBar""" + + def setUp(self): + super(TestColorbarWidget, self).setUp() + self.plot = Plot2D() + self.colorBar = ColorBarWidget(parent=None, plot=self.plot) + + def tearDown(self): + del self.colorBar + self.plot.close() + del self.plot + + super(TestColorbarWidget, self).tearDown() + + def testEmptyColorBar(self): + colorBar = ColorBarWidget(parent=None) + colorBar.show() + self.qWaitForWindowExposed(colorBar) + + def testNegativeColormaps(self): + """test the behavior of the ColorBarWidget in the case of negative + values + + Note : colorbar is modified by the Plot directly not ColorBarWidget + """ + colormapLog = { 'name': 'gray', 'normalization': 'log', + 'autoscale': True, 'vmin': -1.0, 'vmax': 1.0 } + + colormapLog2 = { 'name': 'gray', 'normalization': 'log', + 'autoscale': False, 'vmin': -1.0, 'vmax': 1.0 } + + data = numpy.array([-5, -4, 0, 2, 3, 5, 10, 20, 30]) + data = data.reshape(3, 3) + self.plot.addImage(data=data, colormap=colormapLog, legend='toto') + self.plot.setActiveImage('toto') + + # default behavior when autoscale : set to minmal positive value + data[data<1] = data.max() + self.assertTrue(self.colorBar._colormap['vmin'] == data.min()) + self.assertTrue(self.colorBar._colormap['vmax'] == data.max()) + + data = numpy.linspace(-9, -2, 100).reshape(10, 10) + + self.plot.addImage(data=data, colormap=colormapLog2, legend='toto') + self.plot.setActiveImage('toto') + # if negative values, changing bounds for default : 1, 10 + self.assertTrue(self.colorBar._colormap['vmin'] == 1) + self.assertTrue(self.colorBar._colormap['vmax'] == 10) + + def testPlotAssocation(self): + """Make sure the ColorBarWidget is proparly connected with the plot""" + colormap = { 'name': 'gray', 'normalization': 'linear', + 'autoscale': True, 'vmin': -1.0, 'vmax': 1.0 } + + # make sure that default settings are the same + self.assertTrue( + self.colorBar.getColormap() == self.plot.getDefaultColormap()) + + data = numpy.linspace(0, 10, 100).reshape(10, 10) + self.plot.addImage(data=data, colormap=colormap, legend='toto') + self.plot.setActiveImage('toto') + + # make sure the modification of the colormap has been done + self.assertFalse( + self.colorBar.getColormap() == self.plot.getDefaultColormap()) + + +def suite(): + test_suite = unittest.TestSuite() + for ui in (TestColorScale, TestNoAutoscale, TestColorbarWidget): + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(ui)) + + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
\ No newline at end of file diff --git a/silx/gui/plot/test/testColormapDialog.py b/silx/gui/plot/test/testColormapDialog.py new file mode 100644 index 0000000..d016548 --- /dev/null +++ b/silx/gui/plot/test/testColormapDialog.py @@ -0,0 +1,68 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016 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. +# +# ###########################################################################*/ +"""Basic tests for ColormapDialog""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "05/12/2016" + + +import doctest +import unittest + +from silx.gui.test.utils import qWaitForWindowExposedAndActivate +from silx.gui import qt +from silx.gui.plot import ColormapDialog + + +# Makes sure a QApplication exists +_qapp = qt.QApplication.instance() or qt.QApplication([]) + + +def _tearDownQt(docTest): + """Tear down to use for test from docstring. + + Checks that dialog widget is displayed + """ + dialogWidget = docTest.globs['dialog'] + qWaitForWindowExposedAndActivate(dialogWidget) + dialogWidget.setAttribute(qt.Qt.WA_DeleteOnClose) + dialogWidget.close() + del dialogWidget + _qapp.processEvents() + + +cmapDocTestSuite = doctest.DocTestSuite(ColormapDialog, tearDown=_tearDownQt) +"""Test suite of tests from the module's docstrings.""" + + +def suite(): + test_suite = unittest.TestSuite() + test_suite.addTest(cmapDocTestSuite) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/gui/plot/test/testColors.py b/silx/gui/plot/test/testColors.py new file mode 100644 index 0000000..94c22f3 --- /dev/null +++ b/silx/gui/plot/test/testColors.py @@ -0,0 +1,94 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-2017 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. +# +# ###########################################################################*/ +"""Basic tests for Colors""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "05/12/2016" + + +import numpy + +import unittest +from silx.test.utils import ParametricTestCase + +from silx.gui.plot import Colors + + +class TestRGBA(ParametricTestCase): + """Basic tests of rgba function""" + + def testRGBA(self): + """"Test rgba function with accepted values""" + tests = { # name: (colors, expected values) + 'blue': ('blue', (0., 0., 1., 1.)), + '#010203': ('#010203', (1. / 255., 2. / 255., 3. / 255., 1.)), + '#01020304': ('#01020304', (1. / 255., 2. / 255., 3. / 255., 4. / 255.)), + '3 x uint8': (numpy.array((1, 255, 0), dtype=numpy.uint8), + (1 / 255., 1., 0., 1.)), + '4 x uint8': (numpy.array((1, 255, 0, 1), dtype=numpy.uint8), + (1 / 255., 1., 0., 1 / 255.)), + '3 x float overflow': ((3., 0.5, 1.), (1., 0.5, 1., 1.)), + } + + for name, test in tests.items(): + color, expected = test + with self.subTest(msg=name): + result = Colors.rgba(color) + self.assertEqual(result, expected) + + +class TestApplyColormapToData(ParametricTestCase): + """Tests of applyColormapToData function""" + + def testApplyColormapToData(self): + """Simple test of applyColormapToData function""" + colormap = dict(name='gray', normalization='linear', + autoscale=False, vmin=0, vmax=255) + + size = 10 + expected = numpy.empty((size, 4), dtype='uint8') + expected[:, 0] = numpy.arange(size, dtype='uint8') + expected[:, 1] = expected[:, 0] + expected[:, 2] = expected[:, 0] + expected[:, 3] = 255 + + for dtype in ('uint8', 'int32', 'float32', 'float64'): + with self.subTest(dtype=dtype): + array = numpy.arange(size, dtype=dtype) + result = Colors.applyColormapToData(array, **colormap) + self.assertTrue(numpy.all(numpy.equal(result, expected))) + + +def suite(): + test_suite = unittest.TestSuite() + for testClass in (TestRGBA, TestApplyColormapToData): + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(testClass)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/gui/plot/test/testCurvesROIWidget.py b/silx/gui/plot/test/testCurvesROIWidget.py new file mode 100644 index 0000000..3c6f2ba --- /dev/null +++ b/silx/gui/plot/test/testCurvesROIWidget.py @@ -0,0 +1,153 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016 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. +# +# ###########################################################################*/ +"""Basic tests for CurvesROIWidget""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "05/12/2016" + + +import logging +import os.path +import unittest + +import numpy + +from silx.gui import qt +from silx.test.utils import temp_dir +from silx.gui.test.utils import TestCaseQt +from silx.gui.plot import PlotWindow, CurvesROIWidget + + +logging.basicConfig() +_logger = logging.getLogger(__name__) + + +class TestCurvesROIWidget(TestCaseQt): + """Basic test for CurvesROIWidget""" + + def setUp(self): + super(TestCurvesROIWidget, self).setUp() + self.plot = PlotWindow() + self.plot.show() + self.qWaitForWindowExposed(self.plot) + + self.widget = CurvesROIWidget.CurvesROIDockWidget(plot=self.plot, name='TEST') + self.widget.show() + self.qWaitForWindowExposed(self.widget) + + def tearDown(self): + self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) + self.plot.close() + del self.plot + + self.widget.setAttribute(qt.Qt.WA_DeleteOnClose) + self.widget.close() + del self.widget + + super(TestCurvesROIWidget, self).tearDown() + + def testEmptyPlot(self): + """Empty plot, display ROI widget""" + pass + + def testWithCurves(self): + """Plot with curves: test all ROI widget buttons""" + for offset in range(2): + self.plot.addCurve(numpy.arange(1000), + offset + numpy.random.random(1000), + legend=str(offset)) + + # Add two ROI + self.mouseClick(self.widget.roiWidget.addButton, qt.Qt.LeftButton) + self.mouseClick(self.widget.roiWidget.addButton, qt.Qt.LeftButton) + + # Change active curve + self.plot.setActiveCurve(str(1)) + + # Delete a ROI + self.mouseClick(self.widget.roiWidget.delButton, qt.Qt.LeftButton) + + with temp_dir() as tmpDir: + self.tmpFile = os.path.join(tmpDir, 'test.ini') + + # Save ROIs + self.widget.roiWidget.save(self.tmpFile) + self.assertTrue(os.path.isfile(self.tmpFile)) + + # Reset ROIs + self.mouseClick(self.widget.roiWidget.resetButton, + qt.Qt.LeftButton) + + # Load ROIs + self.widget.roiWidget.load(self.tmpFile) + + del self.tmpFile + + def testCalculation(self): + x = numpy.arange(100.) + y = numpy.arange(100.) + + # Add two curves + self.plot.addCurve(x, y, legend="positive") + self.plot.addCurve(-x, y, legend="negative") + + # Make sure there is an active curve and it is the positive one + self.plot.setActiveCurve("positive") + + # Add two ROIs + ddict = {} + ddict["positive"] = {"from": 10, "to": 20, "type":"X"} + ddict["negative"] = {"from": -20, "to": -10, "type":"X"} + self.widget.roiWidget.setRois(ddict) + + # And calculate the expected output + self.widget.calculateROIs() + + output = self.widget.roiWidget.getRois() + self.assertEqual(output["positive"]["rawcounts"], + y[ddict["positive"]["from"]:ddict["positive"]["to"]+1].sum(), + "Calculation failed on positive X coordinates") + + # Set the curve with negative X coordinates as active + self.plot.setActiveCurve("negative") + + # the ROIs should have been automatically updated + output = self.widget.roiWidget.getRois() + selection = numpy.nonzero((-x >= output["negative"]["from"]) & \ + (-x <= output["negative"]["to"]))[0] + self.assertEqual(output["negative"]["rawcounts"], + y[selection].sum(), "Calculation failed on negative X coordinates") + +def suite(): + test_suite = unittest.TestSuite() + for TestClass in (TestCurvesROIWidget,): + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestClass)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/gui/plot/test/testInteraction.py b/silx/gui/plot/test/testInteraction.py new file mode 100644 index 0000000..074a7cd --- /dev/null +++ b/silx/gui/plot/test/testInteraction.py @@ -0,0 +1,89 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016 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. +# +# ###########################################################################*/ +"""Tests from interaction state machines""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "18/02/2016" + + +import unittest + +from silx.gui.plot import Interaction + + +class TestInteraction(unittest.TestCase): + def testClickOrDrag(self): + """Minimalistic test for click or drag state machine.""" + events = [] + + class TestClickOrDrag(Interaction.ClickOrDrag): + def click(self, x, y, btn): + events.append(('click', x, y, btn)) + + def beginDrag(self, x, y): + events.append(('beginDrag', x, y)) + + def drag(self, x, y): + events.append(('drag', x, y)) + + def endDrag(self, x, y): + events.append(('endDrag', x, y)) + + clickOrDrag = TestClickOrDrag() + + # click + clickOrDrag.handleEvent('press', 10, 10, Interaction.LEFT_BTN) + self.assertEqual(len(events), 0) + + clickOrDrag.handleEvent('release', 10, 10, Interaction.LEFT_BTN) + self.assertEqual(len(events), 1) + self.assertEqual(events[0], ('click', 10, 10, Interaction.LEFT_BTN)) + + # drag + events = [] + clickOrDrag.handleEvent('press', 10, 10, Interaction.LEFT_BTN) + self.assertEqual(len(events), 0) + clickOrDrag.handleEvent('move', 15, 10) + self.assertEqual(len(events), 2) # Received beginDrag and drag + self.assertEqual(events[0], ('beginDrag', 10, 10)) + self.assertEqual(events[1], ('drag', 15, 10)) + clickOrDrag.handleEvent('move', 20, 10) + self.assertEqual(len(events), 3) + self.assertEqual(events[-1], ('drag', 20, 10)) + clickOrDrag.handleEvent('release', 20, 10, Interaction.LEFT_BTN) + self.assertEqual(len(events), 4) + self.assertEqual(events[-1], ('endDrag', (10, 10), (20, 10))) + + +def suite(): + test_suite = unittest.TestSuite() + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestInteraction)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/gui/plot/test/testLegendSelector.py b/silx/gui/plot/test/testLegendSelector.py new file mode 100644 index 0000000..371197f --- /dev/null +++ b/silx/gui/plot/test/testLegendSelector.py @@ -0,0 +1,143 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2004-2016 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. +# +# ###########################################################################*/ +"""Basic tests for PlotWidget""" + +__authors__ = ["T. Rueter", "T. Vincent"] +__license__ = "MIT" +__date__ = "05/12/2016" + + +import logging +import unittest + +from silx.gui import qt +from silx.gui.test.utils import TestCaseQt +from silx.gui.plot import LegendSelector + + +logging.basicConfig() +_logger = logging.getLogger(__name__) + + +class TestLegendSelector(TestCaseQt): + """Basic test for LegendSelector""" + + def testLegendSelector(self): + """Test copied from __main__ of LegendSelector in PyMca""" + class Notifier(qt.QObject): + def __init__(self): + qt.QObject.__init__(self) + self.chk = True + + def signalReceived(self, **kw): + obj = self.sender() + _logger.info('NOTIFIER -- signal received\n\tsender: %s', + str(obj)) + + notifier = Notifier() + + legends = ['Legend0', + 'Legend1', + 'Long Legend 2', + 'Foo Legend 3', + 'Even Longer Legend 4', + 'Short Leg 5', + 'Dot symbol 6', + 'Comma symbol 7'] + colors = [qt.Qt.darkRed, qt.Qt.green, qt.Qt.yellow, qt.Qt.darkCyan, + qt.Qt.blue, qt.Qt.darkBlue, qt.Qt.red, qt.Qt.darkYellow] + symbols = ['o', 't', '+', 'x', 's', 'd', '.', ','] + + win = LegendSelector.LegendListView() + # win = LegendListContextMenu() + # win = qt.QWidget() + # layout = qt.QVBoxLayout() + # layout.setContentsMargins(0,0,0,0) + llist = [] + + for _idx, (l, c, s) in enumerate(zip(legends, colors, symbols)): + ddict = { + 'color': qt.QColor(c), + 'linewidth': 4, + 'symbol': s, + } + legend = l + llist.append((legend, ddict)) + # item = qt.QListWidgetItem(win) + # legendWidget = LegendListItemWidget(l) + # legendWidget.icon.setSymbol(s) + # legendWidget.icon.setColor(qt.QColor(c)) + # layout.addWidget(legendWidget) + # win.setItemWidget(item, legendWidget) + + # win = LegendListItemWidget('Some Legend 1') + # print(llist) + model = LegendSelector.LegendModel(legendList=llist) + win.setModel(model) + win.setSelectionModel(qt.QItemSelectionModel(model)) + win.setContextMenu() + # print('Edit triggers: %d'%win.editTriggers()) + + # win = LegendListWidget(None, legends) + # win[0].updateItem(ddict) + # win.setLayout(layout) + win.sigLegendSignal.connect(notifier.signalReceived) + win.show() + + win.clear() + win.setLegendList(llist) + + self.qWaitForWindowExposed(win) + + +class TestRenameCurveDialog(TestCaseQt): + """Basic test for RenameCurveDialog""" + + def testDialog(self): + """Create dialog, change name and press OK""" + self.dialog = LegendSelector.RenameCurveDialog( + None, 'curve1', ['curve1', 'curve2', 'curve3']) + self.dialog.open() + self.qWaitForWindowExposed(self.dialog) + self.keyClicks(self.dialog.lineEdit, 'changed') + self.mouseClick(self.dialog.okButton, qt.Qt.LeftButton) + self.qapp.processEvents() + ret = self.dialog.result() + self.assertEqual(ret, qt.QDialog.Accepted) + newName = self.dialog.getText() + self.assertEqual(newName, 'curve1changed') + del self.dialog + + +def suite(): + test_suite = unittest.TestSuite() + for TestClass in (TestLegendSelector, TestRenameCurveDialog): + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestClass)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/gui/plot/test/testMaskToolsWidget.py b/silx/gui/plot/test/testMaskToolsWidget.py new file mode 100644 index 0000000..0c11928 --- /dev/null +++ b/silx/gui/plot/test/testMaskToolsWidget.py @@ -0,0 +1,295 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-2017 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. +# +# ###########################################################################*/ +"""Basic tests for MaskToolsWidget""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "24/01/2017" + + +import logging +import os.path +import unittest + +import numpy + +from silx.gui import qt +from silx.test.utils import temp_dir, ParametricTestCase +from silx.gui.test.utils import TestCaseQt, getQToolButtonFromAction +from silx.gui.plot import PlotWindow, MaskToolsWidget + +try: + import fabio +except ImportError: + fabio = None + + +logging.basicConfig() +_logger = logging.getLogger(__name__) + + +class TestMaskToolsWidget(TestCaseQt, ParametricTestCase): + """Basic test for MaskToolsWidget""" + + def setUp(self): + super(TestMaskToolsWidget, self).setUp() + self.plot = PlotWindow() + + self.widget = MaskToolsWidget.MaskToolsDockWidget(plot=self.plot, name='TEST') + self.plot.addDockWidget(qt.Qt.BottomDockWidgetArea, self.widget) + + self.plot.show() + self.qWaitForWindowExposed(self.plot) + + self.maskWidget = self.widget.widget() + + def tearDown(self): + del self.maskWidget + del self.widget + + self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) + self.plot.close() + del self.plot + + super(TestMaskToolsWidget, self).tearDown() + + def testEmptyPlot(self): + """Empty plot, display MaskToolsDockWidget, toggle multiple masks""" + self.maskWidget.setMultipleMasks('single') + self.qapp.processEvents() + + self.maskWidget.setMultipleMasks('exclusive') + self.qapp.processEvents() + + def _drag(self): + """Drag from plot center to offset position""" + plot = self.plot.centralWidget() + xCenter, yCenter = plot.width() // 2, plot.height() // 2 + offset = min(plot.width(), plot.height()) // 10 + + pos0 = xCenter, yCenter + pos1 = xCenter + offset, yCenter + offset + + self.mouseMove(plot, pos=pos0) + self.mousePress(plot, qt.Qt.LeftButton, pos=pos0) + self.mouseMove(plot, pos=pos1) + self.mouseRelease(plot, qt.Qt.LeftButton, pos=pos1) + + def _drawPolygon(self): + """Draw a star polygon in the plot""" + plot = self.plot.centralWidget() + x, y = plot.width() // 2, plot.height() // 2 + offset = min(plot.width(), plot.height()) // 10 + + star = [(x, y + offset), + (x - offset, y - offset), + (x + offset, y), + (x - offset, y), + (x + offset, y - offset)] + + for pos in star: + self.mouseMove(plot, pos=pos) + btn = qt.Qt.LeftButton if pos != star[-1] else qt.Qt.RightButton + self.mouseClick(plot, btn, pos=pos) + + def _drawPencil(self): + """Draw a star polygon in the plot""" + plot = self.plot.centralWidget() + x, y = plot.width() // 2, plot.height() // 2 + offset = min(plot.width(), plot.height()) // 10 + + star = [(x, y + offset), + (x - offset, y - offset), + (x + offset, y), + (x - offset, y), + (x + offset, y - offset)] + + self.mouseMove(plot, pos=star[0]) + self.mousePress(plot, qt.Qt.LeftButton, pos=star[0]) + for pos in star: + self.mouseMove(plot, pos=pos) + self.mouseRelease( + plot, qt.Qt.LeftButton, pos=star[-1]) + + def testWithAnImage(self): + """Plot with an image: test MaskToolsWidget interactions""" + + # Add and remove a image (this should enable/disable GUI + change mask) + self.plot.addImage(numpy.random.random(1024**2).reshape(1024, 1024), + legend='test') + self.qapp.processEvents() + + self.plot.remove('test', kind='image') + self.qapp.processEvents() + + tests = [((0, 0), (1, 1)), + ((1000, 1000), (1, 1)), + ((0, 0), (-1, -1)), + ((1000, 1000), (-1, -1))] + + for origin, scale in tests: + with self.subTest(origin=origin, scale=scale): + self.plot.addImage(numpy.arange(1024**2).reshape(1024, 1024), + legend='test', + origin=origin, + scale=scale) + self.qapp.processEvents() + + # Test draw rectangle # + toolButton = getQToolButtonFromAction(self.maskWidget.rectAction) + self.assertIsNot(toolButton, None) + self.mouseClick(toolButton, qt.Qt.LeftButton) + + # mask + self.maskWidget.maskStateGroup.button(1).click() + self.qapp.processEvents() + self._drag() + self.assertFalse( + numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))) + + # unmask same region + self.maskWidget.maskStateGroup.button(0).click() + self.qapp.processEvents() + self._drag() + self.assertTrue( + numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))) + + # Test draw polygon # + toolButton = getQToolButtonFromAction(self.maskWidget.polygonAction) + self.assertIsNot(toolButton, None) + self.mouseClick(toolButton, qt.Qt.LeftButton) + + # mask + self.maskWidget.maskStateGroup.button(1).click() + self.qapp.processEvents() + self._drawPolygon() + self.assertFalse( + numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))) + + # unmask same region + self.maskWidget.maskStateGroup.button(0).click() + self.qapp.processEvents() + self._drawPolygon() + self.assertTrue( + numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))) + + # Test draw pencil # + toolButton = getQToolButtonFromAction(self.maskWidget.pencilAction) + self.assertIsNot(toolButton, None) + self.mouseClick(toolButton, qt.Qt.LeftButton) + + self.maskWidget.pencilSpinBox.setValue(10) + self.qapp.processEvents() + + # mask + self.maskWidget.maskStateGroup.button(1).click() + self.qapp.processEvents() + self._drawPencil() + self.assertFalse( + numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))) + + # unmask same region + self.maskWidget.maskStateGroup.button(0).click() + self.qapp.processEvents() + self._drawPencil() + self.assertTrue( + numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))) + + # Test no draw tool # + toolButton = getQToolButtonFromAction(self.maskWidget.browseAction) + self.assertIsNot(toolButton, None) + self.mouseClick(toolButton, qt.Qt.LeftButton) + + self.plot.clear() + + def __loadSave(self, file_format): + """Plot with an image: test MaskToolsWidget operations""" + self.plot.addImage(numpy.arange(1024**2).reshape(1024, 1024), + legend='test') + self.qapp.processEvents() + + # Draw a polygon mask + toolButton = getQToolButtonFromAction(self.maskWidget.polygonAction) + self.assertIsNot(toolButton, None) + self.mouseClick(toolButton, qt.Qt.LeftButton) + self._drawPolygon() + + ref_mask = self.maskWidget.getSelectionMask() + self.assertFalse(numpy.all(numpy.equal(ref_mask, 0))) + + with temp_dir() as tmp: + mask_filename = os.path.join(tmp, 'mask.' + file_format) + self.maskWidget.save(mask_filename, file_format) + + self.maskWidget.resetSelectionMask() + self.assertTrue( + numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))) + + self.maskWidget.load(mask_filename) + self.assertTrue(numpy.all(numpy.equal( + self.maskWidget.getSelectionMask(), ref_mask))) + + def testLoadSaveNpy(self): + self.__loadSave("npy") + + def testLoadSaveFit2D(self): + if fabio is None: + self.skipTest("Fabio is missing") + self.__loadSave("msk") + + def testSigMaskChangedEmitted(self): + self.plot.addImage(numpy.arange(512**2).reshape(512, 512), + legend='test') + self.plot.resetZoom() + self.qapp.processEvents() + + l = [] + + def slot(): + l.append(1) + + self.maskWidget.sigMaskChanged.connect(slot) + + # rectangle mask + toolButton = getQToolButtonFromAction(self.maskWidget.rectAction) + self.assertIsNot(toolButton, None) + self.mouseClick(toolButton, qt.Qt.LeftButton) + self.maskWidget.maskStateGroup.button(1).click() + self.qapp.processEvents() + self._drag() + + self.assertGreater(len(l), 0) + + +def suite(): + test_suite = unittest.TestSuite() + for TestClass in (TestMaskToolsWidget,): + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestClass)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/gui/plot/test/testPlot.py b/silx/gui/plot/test/testPlot.py new file mode 100644 index 0000000..25e7511 --- /dev/null +++ b/silx/gui/plot/test/testPlot.py @@ -0,0 +1,633 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-2017 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. +# +# ###########################################################################*/ +"""Basic tests for Plot""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "05/12/2016" + + +import unittest +from functools import reduce +from silx.test.utils import ParametricTestCase + +import numpy + +from silx.gui.plot.Plot import Plot +from silx.gui.plot.items.histogram import _getHistogramCurve, _computeEdges + + +class TestPlot(unittest.TestCase): + """Basic tests of Plot without backend""" + + def testPlotTitleLabels(self): + """Create a Plot and set the labels""" + + plot = Plot(backend='none') + + title, xlabel, ylabel = 'the title', 'x label', 'y label' + plot.setGraphTitle(title) + plot.setGraphXLabel(xlabel) + plot.setGraphYLabel(ylabel) + + self.assertEqual(plot.getGraphTitle(), title) + self.assertEqual(plot.getGraphXLabel(), xlabel) + self.assertEqual(plot.getGraphYLabel(), ylabel) + + def testAddNoRemove(self): + """add objects to the Plot""" + + plot = Plot(backend='none') + plot.addCurve(x=(1, 2, 3), y=(3, 2, 1)) + plot.addImage(numpy.arange(100.).reshape(10, -1)) + plot.addItem( + numpy.array((1., 10.)), numpy.array((10., 10.)), shape="rectangle") + plot.addXMarker(10.) + + +class TestPlotRanges(ParametricTestCase): + """Basic tests of Plot data ranges without backend""" + + _getValidValues = {True: lambda ar: ar > 0, + False: lambda ar: numpy.ones(shape=ar.shape, + dtype=bool)} + + @staticmethod + def _getRanges(arrays, are_logs): + gen = (TestPlotRanges._getValidValues[is_log](ar) + for (ar, is_log) in zip(arrays, are_logs)) + indices = numpy.where(reduce(numpy.logical_and, gen))[0] + if len(indices) > 0: + ranges = [(ar[indices[0]], ar[indices[-1]]) for ar in arrays] + else: + ranges = [None] * len(arrays) + + return ranges + + @staticmethod + def _getRangesMinmax(ranges): + # TODO : error if None in ranges. + rangeMin = numpy.min([rng[0] for rng in ranges]) + rangeMax = numpy.max([rng[1] for rng in ranges]) + return rangeMin, rangeMax + + def testDataRangeNoPlot(self): + """empty plot data range""" + + plot = Plot(backend='none') + + for logX, logY in ((False, False), + (True, False), + (True, True), + (False, True), + (False, False)): + with self.subTest(logX=logX, logY=logY): + plot.setXAxisLogarithmic(logX) + plot.setYAxisLogarithmic(logY) + dataRange = plot.getDataRange() + self.assertIsNone(dataRange.x) + self.assertIsNone(dataRange.y) + self.assertIsNone(dataRange.yright) + + def testDataRangeLeft(self): + """left axis range""" + + plot = Plot(backend='none') + + xData = numpy.arange(10) - 4.9 # range : -4.9 , 4.1 + yData = numpy.arange(10) - 6.9 # range : -6.9 , 2.1 + + plot.addCurve(x=xData, + y=yData, + legend='plot_0', + yaxis='left') + + for logX, logY in ((False, False), + (True, False), + (True, True), + (False, True), + (False, False)): + with self.subTest(logX=logX, logY=logY): + plot.setXAxisLogarithmic(logX) + plot.setYAxisLogarithmic(logY) + dataRange = plot.getDataRange() + xRange, yRange = self._getRanges([xData, yData], + [logX, logY]) + self.assertSequenceEqual(dataRange.x, xRange) + self.assertSequenceEqual(dataRange.y, yRange) + self.assertIsNone(dataRange.yright) + + def testDataRangeRight(self): + """right axis range""" + + plot = Plot(backend='none') + xData = numpy.arange(10) - 4.9 # range : -4.9 , 4.1 + yData = numpy.arange(10) - 6.9 # range : -6.9 , 2.1 + plot.addCurve(x=xData, + y=yData, + legend='plot_0', + yaxis='right') + + for logX, logY in ((False, False), + (True, False), + (True, True), + (False, True), + (False, False)): + with self.subTest(logX=logX, logY=logY): + plot.setXAxisLogarithmic(logX) + plot.setYAxisLogarithmic(logY) + dataRange = plot.getDataRange() + xRange, yRange = self._getRanges([xData, yData], + [logX, logY]) + self.assertSequenceEqual(dataRange.x, xRange) + self.assertIsNone(dataRange.y) + self.assertSequenceEqual(dataRange.yright, yRange) + + def testDataRangeImage(self): + """image data range""" + + origin = (-10, 25) + scale = (3., 8.) + image = numpy.arange(100.).reshape(20, 5) + + plot = Plot(backend='none') + plot.addImage(image, + origin=origin, scale=scale) + + xRange = numpy.array([0., image.shape[1] * scale[0]]) + origin[0] + yRange = numpy.array([0., image.shape[0] * scale[1]]) + origin[1] + + ranges = {(False, False): (xRange, yRange), + (True, False): (None, None), + (True, True): (None, None), + (False, True): (None, None)} + + for logX, logY in ((False, False), + (True, False), + (True, True), + (False, True), + (False, False)): + with self.subTest(logX=logX, logY=logY): + plot.setXAxisLogarithmic(logX) + plot.setYAxisLogarithmic(logY) + dataRange = plot.getDataRange() + xRange, yRange = ranges[logX, logY] + self.assertTrue(numpy.array_equal(dataRange.x, xRange), + msg='{0} != {1}'.format(dataRange.x, xRange)) + self.assertTrue(numpy.array_equal(dataRange.y, yRange), + msg='{0} != {1}'.format(dataRange.y, yRange)) + self.assertIsNone(dataRange.yright) + + def testDataRangeLeftRight(self): + """right+left axis range""" + + plot = Plot(backend='none') + + xData_l = numpy.arange(10) - 0.9 # range : -0.9 , 8.1 + yData_l = numpy.arange(10) - 1.9 # range : -1.9 , 7.1 + plot.addCurve(x=xData_l, + y=yData_l, + legend='plot_l', + yaxis='left') + + xData_r = numpy.arange(10) - 4.9 # range : -4.9 , 4.1 + yData_r = numpy.arange(10) - 6.9 # range : -6.9 , 2.1 + plot.addCurve(x=xData_r, + y=yData_r, + legend='plot_r', + yaxis='right') + + for logX, logY in ((False, False), + (True, False), + (True, True), + (False, True), + (False, False)): + with self.subTest(logX=logX, logY=logY): + plot.setXAxisLogarithmic(logX) + plot.setYAxisLogarithmic(logY) + dataRange = plot.getDataRange() + xRangeL, yRangeL = self._getRanges([xData_l, yData_l], + [logX, logY]) + xRangeR, yRangeR = self._getRanges([xData_r, yData_r], + [logX, logY]) + xRangeLR = self._getRangesMinmax([xRangeL, xRangeR]) + self.assertSequenceEqual(dataRange.x, xRangeLR) + self.assertSequenceEqual(dataRange.y, yRangeL) + self.assertSequenceEqual(dataRange.yright, yRangeR) + + def testDataRangeCurveImage(self): + """right+left+image axis range""" + + # overlapping ranges : + # image sets x min and y max + # plot_left sets y min + # plot_right sets x max (and yright) + plot = Plot(backend='none') + + origin = (-10, 5) + scale = (3., 8.) + image = numpy.arange(100.).reshape(20, 5) + + plot.addImage(image, + origin=origin, scale=scale, legend='image') + + xData_l = numpy.arange(10) - 0.9 # range : -0.9 , 8.1 + yData_l = numpy.arange(10) - 1.9 # range : -1.9 , 7.1 + plot.addCurve(x=xData_l, + y=yData_l, + legend='plot_l', + yaxis='left') + + xData_r = numpy.arange(10) + 4.1 # range : 4.1 , 13.1 + yData_r = numpy.arange(10) - 0.9 # range : -0.9 , 8.1 + plot.addCurve(x=xData_r, + y=yData_r, + legend='plot_r', + yaxis='right') + + imgXRange = numpy.array([0., image.shape[1] * scale[0]]) + origin[0] + imgYRange = numpy.array([0., image.shape[0] * scale[1]]) + origin[1] + + for logX, logY in ((False, False), + (True, False), + (True, True), + (False, True), + (False, False)): + with self.subTest(logX=logX, logY=logY): + plot.setXAxisLogarithmic(logX) + plot.setYAxisLogarithmic(logY) + dataRange = plot.getDataRange() + xRangeL, yRangeL = self._getRanges([xData_l, yData_l], + [logX, logY]) + xRangeR, yRangeR = self._getRanges([xData_r, yData_r], + [logX, logY]) + if logX or logY: + xRangeLR = self._getRangesMinmax([xRangeL, xRangeR]) + else: + xRangeLR = self._getRangesMinmax([xRangeL, + xRangeR, + imgXRange]) + yRangeL = self._getRangesMinmax([yRangeL, imgYRange]) + self.assertSequenceEqual(dataRange.x, xRangeLR) + self.assertSequenceEqual(dataRange.y, yRangeL) + self.assertSequenceEqual(dataRange.yright, yRangeR) + + def testDataRangeImageNegativeScaleX(self): + """image data range, negative scale""" + + origin = (-10, 25) + scale = (-3., 8.) + image = numpy.arange(100.).reshape(20, 5) + + plot = Plot(backend='none') + plot.addImage(image, + origin=origin, scale=scale) + + xRange = numpy.array([0., image.shape[1] * scale[0]]) + origin[0] + xRange.sort() # negative scale! + yRange = numpy.array([0., image.shape[0] * scale[1]]) + origin[1] + + ranges = {(False, False): (xRange, yRange), + (True, False): (None, None), + (True, True): (None, None), + (False, True): (None, None)} + + for logX, logY in ((False, False), + (True, False), + (True, True), + (False, True), + (False, False)): + with self.subTest(logX=logX, logY=logY): + plot.setXAxisLogarithmic(logX) + plot.setYAxisLogarithmic(logY) + dataRange = plot.getDataRange() + xRange, yRange = ranges[logX, logY] + self.assertTrue(numpy.array_equal(dataRange.x, xRange), + msg='{0} != {1}'.format(dataRange.x, xRange)) + self.assertTrue(numpy.array_equal(dataRange.y, yRange), + msg='{0} != {1}'.format(dataRange.y, yRange)) + self.assertIsNone(dataRange.yright) + + def testDataRangeImageNegativeScaleY(self): + """image data range, negative scale""" + + origin = (-10, 25) + scale = (3., -8.) + image = numpy.arange(100.).reshape(20, 5) + + plot = Plot(backend='none') + plot.addImage(image, + origin=origin, scale=scale) + + xRange = numpy.array([0., image.shape[1] * scale[0]]) + origin[0] + yRange = numpy.array([0., image.shape[0] * scale[1]]) + origin[1] + yRange.sort() # negative scale! + + ranges = {(False, False): (xRange, yRange), + (True, False): (None, None), + (True, True): (None, None), + (False, True): (None, None)} + + for logX, logY in ((False, False), + (True, False), + (True, True), + (False, True), + (False, False)): + with self.subTest(logX=logX, logY=logY): + plot.setXAxisLogarithmic(logX) + plot.setYAxisLogarithmic(logY) + dataRange = plot.getDataRange() + xRange, yRange = ranges[logX, logY] + self.assertTrue(numpy.array_equal(dataRange.x, xRange), + msg='{0} != {1}'.format(dataRange.x, xRange)) + self.assertTrue(numpy.array_equal(dataRange.y, yRange), + msg='{0} != {1}'.format(dataRange.y, yRange)) + self.assertIsNone(dataRange.yright) + + def testDataRangeHiddenCurve(self): + """curves with a hidden curve""" + plot = Plot(backend='none') + plot.addCurve((0, 1), (0, 1), legend='shown') + plot.addCurve((0, 1, 2), (5, 5, 5), legend='hidden') + range1 = plot.getDataRange() + self.assertEqual(range1.x, (0, 2)) + self.assertEqual(range1.y, (0, 5)) + plot.hideCurve('hidden') + range2 = plot.getDataRange() + self.assertEqual(range2.x, (0, 1)) + self.assertEqual(range2.y, (0, 1)) + + +class TestPlotGetCurveImage(unittest.TestCase): + """Test of plot getCurve and getImage methods""" + + def testGetCurve(self): + """Plot.getCurve and Plot.getActiveCurve tests""" + + plot = Plot(backend='none') + + # No curve + curve = plot.getCurve() + self.assertIsNone(curve) # No curve + + plot.setActiveCurveHandling(True) + plot.addCurve(x=(0, 1), y=(0, 1), legend='curve 0') + plot.addCurve(x=(0, 1), y=(0, 1), legend='curve 1') + plot.addCurve(x=(0, 1), y=(0, 1), legend='curve 2') + plot.setActiveCurve('curve 0') + + # Active curve + active = plot.getActiveCurve() + self.assertEqual(active.getLegend(), 'curve 0') + curve = plot.getCurve() + self.assertEqual(curve.getLegend(), 'curve 0') + + # No active curve and curves + plot.setActiveCurveHandling(False) + active = plot.getActiveCurve() + self.assertIsNone(active) # No active curve + curve = plot.getCurve() + self.assertEqual(curve.getLegend(), 'curve 2') # Last added curve + + # Last curve hidden + plot.hideCurve('curve 2', True) + curve = plot.getCurve() + self.assertEqual(curve.getLegend(), 'curve 1') # Last added curve + + # All curves hidden + plot.hideCurve('curve 1', True) + plot.hideCurve('curve 0', True) + curve = plot.getCurve() + self.assertIsNone(curve) + + def testGetCurveOldApi(self): + """old API Plot.getCurve and Plot.getActiveCurve tests""" + + plot = Plot(backend='none') + + # No curve + curve = plot.getCurve() + self.assertIsNone(curve) # No curve + + plot.setActiveCurveHandling(True) + x = numpy.arange(10.).astype(numpy.float32) + y = x * x; + plot.addCurve(x=x, y=y, legend='curve 0', info=["whatever"]) + plot.addCurve(x=x, y=2*x, legend='curve 1', info="anything") + plot.setActiveCurve('curve 0') + + # Active curve (4 elements) + xOut, yOut, legend, info = plot.getActiveCurve()[:4] + self.assertEqual(legend, 'curve 0') + self.assertTrue(numpy.allclose(xOut, x), 'curve 0 wrong x data') + self.assertTrue(numpy.allclose(yOut, y), 'curve 0 wrong y data') + + # Active curve (5 elements) + xOut, yOut, legend, info, params = plot.getCurve("curve 1") + self.assertEqual(legend, 'curve 1') + self.assertEqual(info, 'anything') + self.assertTrue(numpy.allclose(xOut, x), 'curve 1 wrong x data') + self.assertTrue(numpy.allclose(yOut, 2*x), 'curve 1 wrong y data') + + def testGetImage(self): + """Plot.getImage and Plot.getActiveImage tests""" + + plot = Plot(backend='none') + + # No image + image = plot.getImage() + self.assertIsNone(image) + + plot.addImage(((0, 1), (2, 3)), legend='image 0', replace=False) + plot.addImage(((0, 1), (2, 3)), legend='image 1', replace=False) + + # Active image + active = plot.getActiveImage() + self.assertEqual(active.getLegend(), 'image 0') + image = plot.getImage() + self.assertEqual(image.getLegend(), 'image 0') + + # No active image + plot.addImage(((0, 1), (2, 3)), legend='image 2', replace=False) + plot.setActiveImage(None) + active = plot.getActiveImage() + self.assertIsNone(active) + image = plot.getImage() + self.assertEqual(image.getLegend(), 'image 2') + + # Active image + plot.setActiveImage('image 1') + active = plot.getActiveImage() + self.assertEqual(active.getLegend(), 'image 1') + image = plot.getImage() + self.assertEqual(image.getLegend(), 'image 1') + + def testGetImageOldApi(self): + """Plot.getImage and Plot.getActiveImage old API tests""" + + plot = Plot(backend='none') + + # No image + image = plot.getImage() + self.assertIsNone(image) + + image = numpy.arange(10).astype(numpy.float32) + image.shape = 5, 2 + + plot.addImage(image, legend='image 0', info=["Hi!"], replace=False) + + # Active image + data, legend, info, something, params = plot.getActiveImage() + self.assertEqual(legend, 'image 0') + self.assertEqual(info, ["Hi!"]) + self.assertTrue(numpy.allclose(data, image), "image 0 data not correct") + + def testGetAllImages(self): + """Plot.getAllImages test""" + + plot = Plot(backend='none') + + # No image + images = plot.getAllImages() + self.assertEqual(len(images), 0) + + # 2 images + data = numpy.arange(100).reshape(10, 10) + plot.addImage(data, legend='1', replace=False) + plot.addImage(data, origin=(10, 10), legend='2', replace=False) + images = plot.getAllImages(just_legend=True) + self.assertEqual(list(images), ['1', '2']) + images = plot.getAllImages(just_legend=False) + self.assertEqual(len(images), 2) + self.assertEqual(images[0].getLegend(), '1') + self.assertEqual(images[1].getLegend(), '2') + + +class TestPlotAddScatter(unittest.TestCase): + """Test of plot addScatter""" + + def testAddGetScatter(self): + + plot = Plot(backend='none') + + # No curve + scatter = plot._getItem(kind="scatter") + self.assertIsNone(scatter) # No curve + + plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 0') + plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 1') + plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 2') + plot._setActiveItem('scatter', 'scatter 0') + + # Active scatter + active = plot._getActiveItem(kind='scatter') + self.assertEqual(active.getLegend(), 'scatter 0') + + # check default values + self.assertAlmostEqual(active.getSymbolSize(), active._DEFAULT_SYMBOL_SIZE) + self.assertEqual(active.getSymbol(), "o") + self.assertAlmostEqual(active.getAlpha(), 1.0) + + # modify parameters + active.setSymbolSize(20.5) + active.setSymbol("d") + active.setAlpha(0.777) + + s0 = plot.getScatter("scatter 0") + + self.assertAlmostEqual(s0.getSymbolSize(), 20.5) + self.assertEqual(s0.getSymbol(), "d") + self.assertAlmostEqual(s0.getAlpha(), 0.777) + + scatter1 = plot._getItem(kind='scatter', legend='scatter 1') + self.assertEqual(scatter1.getLegend(), 'scatter 1') + + def testGetAllScatters(self): + """Plot.getAllImages test""" + + plot = Plot(backend='none') + + scatters = plot._getItems(kind='scatter') + self.assertEqual(len(scatters), 0) + + plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 0') + plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 1') + plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 2') + + scatters = plot._getItems(kind='scatter') + self.assertEqual(len(scatters), 3) + self.assertEqual(scatters[0].getLegend(), 'scatter 0') + self.assertEqual(scatters[2].getLegend(), 'scatter 2') + + scatters = plot._getItems(kind='scatter', just_legend=True) + self.assertEqual(len(scatters), 3) + self.assertEqual(list(scatters), ['scatter 0', 'scatter 1', 'scatter 2']) + + +class TestPlotHistogram(unittest.TestCase): + """Basic tests for histogram.""" + + def testEdges(self): + x = numpy.array([0, 1, 2]) + edgesRight = numpy.array([0, 1, 2, 3]) + edgesLeft = numpy.array([-1, 0, 1, 2]) + edgesCenter = numpy.array([-0.5, 0.5, 1.5, 2.5]) + + # testing x values for right + edges = _computeEdges(x, 'right') + numpy.testing.assert_array_equal(edges, edgesRight) + + edges = _computeEdges(x, 'center') + numpy.testing.assert_array_equal(edges, edgesCenter) + + edges = _computeEdges(x, 'left') + numpy.testing.assert_array_equal(edges, edgesLeft) + + def testHistogramCurve(self): + y = numpy.array([3, 2, 5]) + edges = numpy.array([0, 1, 2, 3]) + + xHisto, yHisto = _getHistogramCurve(y, edges) + numpy.testing.assert_array_equal( + yHisto, numpy.array([3, 3, 2, 2, 5, 5])) + + y = numpy.array([-3, 2, 5, 0]) + edges = numpy.array([-2, -1, 0, 1, 2]) + xHisto, yHisto = _getHistogramCurve(y, edges) + numpy.testing.assert_array_equal( + yHisto, numpy.array([-3, -3, 2, 2, 5, 5, 0, 0])) + + +def suite(): + test_suite = unittest.TestSuite() + for TestClass in (TestPlot, TestPlotRanges, TestPlotGetCurveImage, + TestPlotHistogram, TestPlotAddScatter): + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestClass)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/gui/plot/test/testPlotInteraction.py b/silx/gui/plot/test/testPlotInteraction.py new file mode 100644 index 0000000..25f57a9 --- /dev/null +++ b/silx/gui/plot/test/testPlotInteraction.py @@ -0,0 +1,167 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016 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. +# +# ###########################################################################*/ +"""Tests of plot interaction, through a PlotWidget""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "13/10/2016" + + +import unittest +from silx.gui import qt +from silx.gui.plot.test.testPlotWidget import _PlotWidgetTest + + +class _SignalDump(object): + """Callable object that store passed arguments in a list""" + + def __init__(self): + self._received = [] + + def __call__(self, *args): + self._received.append(args) + + @property + def received(self): + """Return a shallow copy of the list of received arguments""" + return list(self._received) + + +class TestSelectPolygon(_PlotWidgetTest): + """Test polygon selection interaction""" + + def _interactionModeChanged(self, source): + """Check that source received in event is the correct one""" + self.assertEqual(source, self) + + def _draw(self, polygon): + """Draw a polygon in the plot + + :param polygon: List of points (x, y) of the polygon (not closed) + """ + plot = self.plot.centralWidget() + + dump = _SignalDump() + self.plot.sigPlotSignal.connect(dump) + + for pos in polygon: + self.mouseMove(plot, pos=pos) + btn = qt.Qt.LeftButton if pos != polygon[-1] else qt.Qt.RightButton + self.mouseClick(plot, btn, pos=pos) + + self.plot.sigPlotSignal.disconnect(dump) + return [args[0] for args in dump.received] + + def test(self): + """Test draw polygons + events""" + self.plot.sigInteractiveModeChanged.connect( + self._interactionModeChanged) + + self.plot.setInteractiveMode( + 'draw', shape='polygon', label='test', source=self) + interaction = self.plot.getInteractiveMode() + + self.assertEqual(interaction['mode'], 'draw') + self.assertEqual(interaction['shape'], 'polygon') + + self.plot.sigInteractiveModeChanged.disconnect( + self._interactionModeChanged) + + plot = self.plot.centralWidget() + xCenter, yCenter = plot.width() // 2, plot.height() // 2 + offset = min(plot.width(), plot.height()) // 10 + + # Star polygon + star = [(xCenter, yCenter + offset), + (xCenter - offset, yCenter - offset), + (xCenter + offset, yCenter), + (xCenter - offset, yCenter), + (xCenter + offset, yCenter - offset)] + + # Draw while dumping signals + events = self._draw(star) + + # Test last event + drawEvents = [event for event in events + if event['event'].startswith('drawing')] + self.assertEqual(drawEvents[-1]['event'], 'drawingFinished') + self.assertEqual(len(drawEvents[-1]['points']), 6) + + # Large square + largeSquare = [(xCenter - offset, yCenter - offset), + (xCenter + offset, yCenter - offset), + (xCenter + offset, yCenter + offset), + (xCenter - offset, yCenter + offset)] + + # Draw while dumping signals + events = self._draw(largeSquare) + + # Test last event + drawEvents = [event for event in events + if event['event'].startswith('drawing')] + self.assertEqual(drawEvents[-1]['event'], 'drawingFinished') + self.assertEqual(len(drawEvents[-1]['points']), 5) + + # Rectangle too thin along X: Some points are ignored + thinRectX = [(xCenter, yCenter - offset), + (xCenter, yCenter + offset), + (xCenter + 1, yCenter + offset), + (xCenter + 1, yCenter - offset)] + + # Draw while dumping signals + events = self._draw(thinRectX) + + # Test last event + drawEvents = [event for event in events + if event['event'].startswith('drawing')] + self.assertEqual(drawEvents[-1]['event'], 'drawingFinished') + self.assertEqual(len(drawEvents[-1]['points']), 3) + + # Rectangle too thin along Y: Some points are ignored + thinRectY = [(xCenter - offset, yCenter), + (xCenter + offset, yCenter), + (xCenter + offset, yCenter + 1), + (xCenter - offset, yCenter + 1)] + + # Draw while dumping signals + events = self._draw(thinRectY) + + # Test last event + drawEvents = [event for event in events + if event['event'].startswith('drawing')] + self.assertEqual(drawEvents[-1]['event'], 'drawingFinished') + self.assertEqual(len(drawEvents[-1]['points']), 3) + + +def suite(): + test_suite = unittest.TestSuite() + for TestClass in (TestSelectPolygon,): + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestClass)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/gui/plot/test/testPlotTools.py b/silx/gui/plot/test/testPlotTools.py new file mode 100644 index 0000000..1d5e148 --- /dev/null +++ b/silx/gui/plot/test/testPlotTools.py @@ -0,0 +1,203 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-2017 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. +# +# ###########################################################################*/ +"""Basic tests for PlotTools""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "05/12/2016" + + +import numpy +import unittest + +from silx.test.utils import ParametricTestCase, TestLogging +from silx.gui.test.utils import ( + qWaitForWindowExposedAndActivate, TestCaseQt, getQToolButtonFromAction) +from silx.gui import qt +from silx.gui.plot import Plot2D, PlotWindow, PlotTools + + +# 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(TestCaseQt): + """Tests for PositionInfo widget.""" + + def setUp(self): + super(TestPositionInfo, self).setUp() + self.plot = PlotWindow() + self.plot.show() + self.qWaitForWindowExposed(self.plot) + self.mouseMove(self.plot, pos=(1, 1)) + self.qapp.processEvents() + self.qWait(100) + + def tearDown(self): + self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) + self.plot.close() + del self.plot + + super(TestPositionInfo, self).tearDown() + + def _test(self, positionWidget, converterNames, **kwargs): + """General test of PositionInfo. + + - Add it to a toolbar and + - Move mouse around the center of the PlotWindow. + """ + toolBar = qt.QToolBar() + self.plot.addToolBar(qt.Qt.BottomToolBarArea, toolBar) + + toolBar.addWidget(positionWidget) + + converters = positionWidget.getConverters() + self.assertEqual(len(converters), len(converterNames)) + for index, name in enumerate(converterNames): + self.assertEqual(converters[index][0], name) + + with TestLogging(PlotTools.__name__, **kwargs): + # Move mouse to center + self.mouseMove(self.plot) + self.mouseMove(self.plot, pos=(1, 1)) + self.qapp.processEvents() + self.qWait(100) + + def testDefaultConverters(self): + """Test PositionInfo with default converters""" + positionWidget = PlotTools.PositionInfo(plot=self.plot) + self._test(positionWidget, ('X', 'Y')) + + def testCustomConverters(self): + """Test PositionInfo with custom converters""" + converters = [ + ('Coords', lambda x, y: (int(x), int(y))), + ('Radius', lambda x, y: numpy.sqrt(x * x + y * y)), + ('Angle', lambda x, y: numpy.degrees(numpy.arctan2(y, x))) + ] + positionWidget = PlotTools.PositionInfo(plot=self.plot, + converters=converters) + self._test(positionWidget, ('Coords', 'Radius', 'Angle')) + + def testFailingConverters(self): + """Test PositionInfo with failing custom converters""" + def raiseException(x, y): + raise RuntimeError() + + positionWidget = PlotTools.PositionInfo( + plot=self.plot, + converters=[('Exception', raiseException)]) + self._test(positionWidget, ['Exception'], error=2) + + +class TestPixelIntensitiesHisto(TestCaseQt, ParametricTestCase): + """Tests for ProfileToolBar widget.""" + + def setUp(self): + super(TestPixelIntensitiesHisto, self).setUp() + self.image = numpy.random.rand(100, 100) + self.plotImage = Plot2D() + self.plotImage.getIntensityHistogramAction().setVisible(True) + + def tearDown(self): + del self.plotImage + super(TestPixelIntensitiesHisto, self).tearDown() + + def testShowAndHide(self): + """Simple test that the plot is showing and hiding when activating the + action""" + self.plotImage.addImage(self.image, origin=(0, 0), legend='sino') + self.plotImage.show() + + histoAction = self.plotImage.getIntensityHistogramAction() + + # test the pixel intensity diagram is showing + button = getQToolButtonFromAction(histoAction) + self.assertIsNot(button, None) + self.mouseMove(button) + self.mouseClick(button, qt.Qt.LeftButton) + self.qapp.processEvents() + self.assertTrue(histoAction.getHistogramPlotWidget().isVisible()) + + # test the pixel intensity diagram is hiding + self.qapp.setActiveWindow(self.plotImage) + self.qapp.processEvents() + self.mouseMove(button) + self.mouseClick(button, qt.Qt.LeftButton) + self.qapp.processEvents() + self.assertFalse(histoAction.getHistogramPlotWidget().isVisible()) + + def testImageFormatInput(self): + """Test multiple type as image input""" + typesToTest = [numpy.uint8, numpy.int8, numpy.int16, numpy.int32, + numpy.float32, numpy.float64] + self.plotImage.addImage(self.image, origin=(0, 0), legend='sino') + self.plotImage.show() + button = getQToolButtonFromAction( + self.plotImage.getIntensityHistogramAction()) + self.mouseMove(button) + self.mouseClick(button, qt.Qt.LeftButton) + self.qapp.processEvents() + for typeToTest in typesToTest: + with self.subTest(typeToTest=typeToTest): + self.plotImage.addImage(self.image.astype(typeToTest), + origin=(0, 0), legend='sino') + + +def suite(): + test_suite = unittest.TestSuite() + # test_suite.addTest(positionInfoTestSuite) + for testClass in (TestPositionInfo, TestPixelIntensitiesHisto): + test_suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase( + testClass)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/gui/plot/test/testPlotWidget.py b/silx/gui/plot/test/testPlotWidget.py new file mode 100644 index 0000000..2de18a8 --- /dev/null +++ b/silx/gui/plot/test/testPlotWidget.py @@ -0,0 +1,967 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016 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. +# +# ###########################################################################*/ +"""Basic tests for PlotWidget""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "05/12/2016" + + +import unittest + +import numpy + +from silx.test.utils import ParametricTestCase +from silx.gui.test.utils import TestCaseQt + +from silx.gui import qt +from silx.gui.plot import PlotWidget + + +SIZE = 1024 +"""Size of the test image""" + +DATA_2D = numpy.arange(SIZE ** 2).reshape(SIZE, SIZE) +"""Image data set""" + + +class _PlotWidgetTest(TestCaseQt): + """Base class for tests of PlotWidget, not a TestCase in itself. + + plot attribute is the PlotWidget created for the test. + """ + + def setUp(self): + super(_PlotWidgetTest, self).setUp() + self.plot = PlotWidget() + self.plot.show() + self.qWaitForWindowExposed(self.plot) + + def tearDown(self): + self.qapp.processEvents() + self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) + self.plot.close() + del self.plot + super(_PlotWidgetTest, self).tearDown() + + +class TestPlotWidget(_PlotWidgetTest, ParametricTestCase): + """Basic tests for PlotWidget""" + + def testShow(self): + """Most basic test""" + pass + + def testSetTitleLabels(self): + """Set title and axes labels""" + + title, xlabel, ylabel = 'the title', 'x label', 'y label' + self.plot.setGraphTitle(title) + self.plot.setGraphXLabel(xlabel) + self.plot.setGraphYLabel(ylabel) + self.qapp.processEvents() + + self.assertEqual(self.plot.getGraphTitle(), title) + self.assertEqual(self.plot.getGraphXLabel(), xlabel) + self.assertEqual(self.plot.getGraphYLabel(), ylabel) + + def testChangeLimitsWithAspectRatio(self): + def checkLimits(expectedXLim=None, expectedYLim=None, + expectedRatio=None): + xlim = self.plot.getGraphXLimits() + ylim = self.plot.getGraphYLimits() + ratio = abs(xlim[1] - xlim[0]) / abs(ylim[1] - ylim[0]) + + if expectedXLim is not None: + self.assertEqual(expectedXLim, xlim) + + if expectedYLim is not None: + self.assertEqual(expectedYLim, ylim) + + if expectedRatio is not None: + self.assertTrue( + numpy.allclose(expectedRatio, ratio, atol=0.01)) + + self.plot.setKeepDataAspectRatio() + self.qapp.processEvents() + xlim = self.plot.getGraphXLimits() + ylim = self.plot.getGraphYLimits() + defaultRatio = abs(xlim[1] - xlim[0]) / abs(ylim[1] - ylim[0]) + + self.plot.setGraphXLimits(1., 10.) + checkLimits(expectedXLim=(1., 10.), expectedRatio=defaultRatio) + self.qapp.processEvents() + checkLimits(expectedXLim=(1., 10.), expectedRatio=defaultRatio) + + self.plot.setGraphYLimits(1., 10.) + checkLimits(expectedYLim=(1., 10.), expectedRatio=defaultRatio) + self.qapp.processEvents() + checkLimits(expectedYLim=(1., 10.), expectedRatio=defaultRatio) + + +class TestPlotImage(_PlotWidgetTest, ParametricTestCase): + """Basic tests for addImage""" + + def setUp(self): + super(TestPlotImage, self).setUp() + + self.plot.setGraphYLabel('Rows') + self.plot.setGraphXLabel('Columns') + + def testPlotColormapTemperature(self): + self.plot.setGraphTitle('Temp. Linear') + + colormap = {'name': 'temperature', 'normalization': 'linear', + 'autoscale': True, 'vmin': 0.0, 'vmax': 1.0} + self.plot.addImage(DATA_2D, legend="image 1", colormap=colormap) + + def testPlotColormapGray(self): + self.plot.setKeepDataAspectRatio(False) + self.plot.setGraphTitle('Gray Linear') + + colormap = {'name': 'gray', 'normalization': 'linear', + 'autoscale': True, 'vmin': 0.0, 'vmax': 1.0} + self.plot.addImage(DATA_2D, legend="image 1", colormap=colormap) + + def testPlotColormapTemperatureLog(self): + self.plot.setGraphTitle('Temp. Log') + + colormap = {'name': 'temperature', 'normalization': 'log', + 'autoscale': True, 'vmin': 0.0, 'vmax': 1.0} + self.plot.addImage(DATA_2D, legend="image 1", colormap=colormap) + + def testPlotRgbRgba(self): + self.plot.setKeepDataAspectRatio(False) + self.plot.setGraphTitle('RGB + RGBA') + + rgb = numpy.array( + (((0, 0, 0), (128, 0, 0), (255, 0, 0)), + ((0, 128, 0), (0, 128, 128), (0, 128, 256))), + dtype=numpy.uint8) + + self.plot.addImage(rgb, legend="rgb", + origin=(0, 0), scale=(10, 10), + replace=False, resetzoom=False) + + rgba = numpy.array( + (((0, 0, 0, .5), (.5, 0, 0, 1), (1, 0, 0, .5)), + ((0, .5, 0, 1), (0, .5, .5, 1), (0, 1, 1, .5))), + dtype=numpy.float32) + + self.plot.addImage(rgba, legend="rgba", + origin=(5, 5), scale=(10, 10), + replace=False, resetzoom=False) + + self.plot.resetZoom() + + def testPlotColormapCustom(self): + self.plot.setKeepDataAspectRatio(False) + self.plot.setGraphTitle('Custom colormap') + + colormap = {'name': None, 'normalization': 'linear', + 'autoscale': True, 'vmin': 0.0, 'vmax': 1.0, + 'colors': ((0., 0., 0.), (1., 0., 0.), + (0., 1., 0.), (0., 0., 1.))} + self.plot.addImage(DATA_2D, legend="image 1", colormap=colormap, + replace=False, resetzoom=False) + + colormap = {'name': None, 'normalization': 'linear', + 'autoscale': True, 'vmin': 0.0, 'vmax': 1.0, + 'colors': numpy.array( + ((0, 0, 0, 0), (0, 0, 0, 128), + (128, 128, 128, 128), (255, 255, 255, 255)), + dtype=numpy.uint8)} + self.plot.addImage(DATA_2D, legend="image 2", colormap=colormap, + origin=(DATA_2D.shape[0], 0), + replace=False, resetzoom=False) + self.plot.resetZoom() + + def testImageOriginScale(self): + """Test of image with different origin and scale""" + self.plot.setGraphTitle('origin and scale') + + tests = [ # (origin, scale) + ((10, 20), (1, 1)), + ((10, 20), (-1, -1)), + ((-10, 20), (2, 1)), + ((10, -20), (-1, -2)), + (100, 2), + (-100, (1, 1)), + ((10, 20), 2), + ] + + for origin, scale in tests: + with self.subTest(origin=origin, scale=scale): + self.plot.addImage(DATA_2D, origin=origin, scale=scale) + + try: + ox, oy = origin + except TypeError: + ox, oy = origin, origin + try: + sx, sy = scale + except TypeError: + sx, sy = scale, scale + xbounds = ox, ox + DATA_2D.shape[1] * sx + ybounds = oy, oy + DATA_2D.shape[0] * sy + + # Check limits without aspect ratio + xmin, xmax = self.plot.getGraphXLimits() + ymin, ymax = self.plot.getGraphYLimits() + self.assertEqual(xmin, min(xbounds)) + self.assertEqual(xmax, max(xbounds)) + self.assertEqual(ymin, min(ybounds)) + self.assertEqual(ymax, max(ybounds)) + + # Check limits with aspect ratio + self.plot.setKeepDataAspectRatio(True) + xmin, xmax = self.plot.getGraphXLimits() + ymin, ymax = self.plot.getGraphYLimits() + self.assertTrue(xmin <= min(xbounds)) + self.assertTrue(xmax >= max(xbounds)) + self.assertTrue(ymin <= min(ybounds)) + self.assertTrue(ymax >= max(ybounds)) + + self.plot.setKeepDataAspectRatio(False) # Reset aspect ratio + self.plot.clear() + self.plot.resetZoom() + + +class TestPlotCurve(_PlotWidgetTest): + """Basic tests for addCurve.""" + + # Test data sets + xData = numpy.arange(1000) + yData = -500 + 100 * numpy.sin(xData) + xData2 = xData + 1000 + yData2 = xData - 1000 + 200 * numpy.random.random(1000) + + def setUp(self): + super(TestPlotCurve, self).setUp() + self.plot.setGraphTitle('Curve') + self.plot.setGraphYLabel('Rows') + self.plot.setGraphXLabel('Columns') + + self.plot.setActiveCurveHandling(False) + + def testPlotCurveColorFloat(self): + color = numpy.array(numpy.random.random(3 * 1000), + dtype=numpy.float32).reshape(1000, 3) + + self.plot.addCurve(self.xData, self.yData, + legend="curve 1", + replace=False, resetzoom=False, + color=color, + linestyle="", symbol="s") + self.plot.addCurve(self.xData2, self.yData2, + legend="curve 2", + replace=False, resetzoom=False, + color='green', linestyle="-", symbol='o') + self.plot.resetZoom() + + def testPlotCurveColorByte(self): + color = numpy.array(255 * numpy.random.random(3 * 1000), + dtype=numpy.uint8).reshape(1000, 3) + + self.plot.addCurve(self.xData, self.yData, + legend="curve 1", + replace=False, resetzoom=False, + color=color, + linestyle="", symbol="s") + self.plot.addCurve(self.xData2, self.yData2, + legend="curve 2", + replace=False, resetzoom=False, + color='green', linestyle="-", symbol='o') + self.plot.resetZoom() + + def testPlotCurveColors(self): + color = numpy.array(numpy.random.random(3 * 1000), + dtype=numpy.float32).reshape(1000, 3) + + self.plot.addCurve(self.xData, self.yData, + legend="curve 2", + replace=False, resetzoom=False, + color=color, linestyle="-", symbol='o') + self.plot.resetZoom() + + +class TestPlotMarker(_PlotWidgetTest): + """Basic tests for add*Marker""" + + def setUp(self): + super(TestPlotMarker, self).setUp() + self.plot.setGraphYLabel('Rows') + self.plot.setGraphXLabel('Columns') + + self.plot.setXAxisAutoScale(False) + self.plot.setYAxisAutoScale(False) + self.plot.setKeepDataAspectRatio(False) + self.plot.setLimits(0., 100., -100., 100.) + + def testPlotMarkerX(self): + self.plot.setGraphTitle('Markers X') + + markers = [ + (10., 'blue', False, False), + (20., 'red', False, False), + (40., 'green', True, False), + (60., 'gray', True, True), + (80., 'black', False, True), + ] + + for x, color, select, drag in markers: + name = str(x) + if select: + name += " sel." + if drag: + name += " drag" + self.plot.addXMarker(x, name, name, color, select, drag) + self.plot.resetZoom() + + def testPlotMarkerY(self): + self.plot.setGraphTitle('Markers Y') + + markers = [ + (-50., 'blue', False, False), + (-30., 'red', False, False), + (0., 'green', True, False), + (10., 'gray', True, True), + (80., 'black', False, True), + ] + + for y, color, select, drag in markers: + name = str(y) + if select: + name += " sel." + if drag: + name += " drag" + self.plot.addYMarker(y, name, name, color, select, drag) + self.plot.resetZoom() + + def testPlotMarkerPt(self): + self.plot.setGraphTitle('Markers Pt') + + markers = [ + (10., -50., 'blue', False, False), + (40., -30., 'red', False, False), + (50., 0., 'green', True, False), + (50., 20., 'gray', True, True), + (70., 50., 'black', False, True), + ] + for x, y, color, select, drag in markers: + name = "{0},{1}".format(x, y) + if select: + name += " sel." + if drag: + name += " drag" + self.plot.addMarker(x, y, name, name, color, select, drag) + + self.plot.resetZoom() + + def testPlotMarkerWithoutLegend(self): + self.plot.setGraphTitle('Markers without legend') + self.plot.setYAxisInverted(True) + + # Markers without legend + self.plot.addMarker(10, 10) + self.plot.addMarker(10, 20) + self.plot.addMarker(40, 50, text='test', symbol=None) + self.plot.addMarker(40, 50, text='test', symbol='+') + self.plot.addXMarker(25) + self.plot.addXMarker(35) + self.plot.addXMarker(45, text='test') + self.plot.addYMarker(55) + self.plot.addYMarker(65) + self.plot.addYMarker(75, text='test') + + self.plot.resetZoom() + + +# TestPlotItem ################################################################ + +class TestPlotItem(_PlotWidgetTest): + """Basic tests for addItem.""" + + # Polygon coordinates and color + polygons = [ # legend, x coords, y coords, color + ('triangle', numpy.array((10, 30, 50)), + numpy.array((55, 70, 55)), 'red'), + ('square', numpy.array((10, 10, 50, 50)), + numpy.array((10, 50, 50, 10)), 'green'), + ('star', numpy.array((60, 70, 80, 60, 80)), + numpy.array((25, 50, 25, 40, 40)), 'blue'), + ] + + # Rectangle coordinantes and color + rectangles = [ # legend, x coords, y coords, color + ('square 1', numpy.array((1., 10.)), + numpy.array((1., 10.)), 'red'), + ('square 2', numpy.array((10., 20.)), + numpy.array((10., 20.)), 'green'), + ('square 3', numpy.array((20., 30.)), + numpy.array((20., 30.)), 'blue'), + ('rect 1', numpy.array((1., 30.)), + numpy.array((35., 40.)), 'black'), + ('line h', numpy.array((1., 30.)), + numpy.array((45., 45.)), 'darkRed'), + ] + + def setUp(self): + super(TestPlotItem, self).setUp() + + self.plot.setGraphYLabel('Rows') + self.plot.setGraphXLabel('Columns') + self.plot.setXAxisAutoScale(False) + self.plot.setYAxisAutoScale(False) + self.plot.setKeepDataAspectRatio(False) + self.plot.setLimits(0., 100., -100., 100.) + + def testPlotItemPolygonFill(self): + self.plot.setGraphTitle('Item Fill') + + for legend, xList, yList, color in self.polygons: + self.plot.addItem(xList, yList, legend=legend, + replace=False, + shape="polygon", fill=True, color=color) + self.plot.resetZoom() + + def testPlotItemPolygonNoFill(self): + self.plot.setGraphTitle('Item No Fill') + + for legend, xList, yList, color in self.polygons: + self.plot.addItem(xList, yList, legend=legend, + replace=False, + shape="polygon", fill=False, color=color) + self.plot.resetZoom() + + def testPlotItemRectangleFill(self): + self.plot.setGraphTitle('Rectangle Fill') + + for legend, xList, yList, color in self.rectangles: + self.plot.addItem(xList, yList, legend=legend, + replace=False, + shape="rectangle", fill=True, color=color) + self.plot.resetZoom() + + def testPlotItemRectangleNoFill(self): + self.plot.setGraphTitle('Rectangle No Fill') + + for legend, xList, yList, color in self.rectangles: + self.plot.addItem(xList, yList, legend=legend, + replace=False, + shape="rectangle", fill=False, color=color) + self.plot.resetZoom() + + +class TestPlotActiveCurveImage(_PlotWidgetTest): + """Basic tests for active image handling""" + + def testActiveCurveAndLabels(self): + # Active curve handling off, no label change + self.plot.setActiveCurveHandling(False) + self.plot.setGraphXLabel('XLabel') + self.plot.setGraphYLabel('YLabel') + self.plot.addCurve((1, 2), (1, 2)) + self.assertEqual(self.plot.getGraphXLabel(), 'XLabel') + self.assertEqual(self.plot.getGraphYLabel(), 'YLabel') + + self.plot.addCurve((1, 2), (2, 3), xlabel='x1', ylabel='y1') + self.assertEqual(self.plot.getGraphXLabel(), 'XLabel') + self.assertEqual(self.plot.getGraphYLabel(), 'YLabel') + + self.plot.clear() + self.assertEqual(self.plot.getGraphXLabel(), 'XLabel') + self.assertEqual(self.plot.getGraphYLabel(), 'YLabel') + + # Active curve handling on, label changes + self.plot.setActiveCurveHandling(True) + self.plot.setGraphXLabel('XLabel') + self.plot.setGraphYLabel('YLabel') + + # labels changed as active curve + self.plot.addCurve((1, 2), (1, 2), legend='1', + xlabel='x1', ylabel='y1') + self.assertEqual(self.plot.getGraphXLabel(), 'x1') + self.assertEqual(self.plot.getGraphYLabel(), 'y1') + + # labels not changed as not active curve + self.plot.addCurve((1, 2), (2, 3), legend='2') + self.assertEqual(self.plot.getGraphXLabel(), 'x1') + self.assertEqual(self.plot.getGraphYLabel(), 'y1') + + # labels changed + self.plot.setActiveCurve('2') + self.assertEqual(self.plot.getGraphXLabel(), 'XLabel') + self.assertEqual(self.plot.getGraphYLabel(), 'YLabel') + + self.plot.setActiveCurve('1') + self.assertEqual(self.plot.getGraphXLabel(), 'x1') + self.assertEqual(self.plot.getGraphYLabel(), 'y1') + + self.plot.clear() + self.assertEqual(self.plot.getGraphXLabel(), 'XLabel') + self.assertEqual(self.plot.getGraphYLabel(), 'YLabel') + + def testActiveImageAndLabels(self): + # Active image handling always on, no API for toggling it + self.plot.setGraphXLabel('XLabel') + self.plot.setGraphYLabel('YLabel') + + # labels changed as active curve + self.plot.addImage(numpy.arange(100).reshape(10, 10), replace=False, + legend='1', xlabel='x1', ylabel='y1') + self.assertEqual(self.plot.getGraphXLabel(), 'x1') + self.assertEqual(self.plot.getGraphYLabel(), 'y1') + + # labels not changed as not active curve + self.plot.addImage(numpy.arange(100).reshape(10, 10), replace=False, + legend='2') + self.assertEqual(self.plot.getGraphXLabel(), 'x1') + self.assertEqual(self.plot.getGraphYLabel(), 'y1') + + # labels changed + self.plot.setActiveImage('2') + self.assertEqual(self.plot.getGraphXLabel(), 'XLabel') + self.assertEqual(self.plot.getGraphYLabel(), 'YLabel') + + self.plot.setActiveImage('1') + self.assertEqual(self.plot.getGraphXLabel(), 'x1') + self.assertEqual(self.plot.getGraphYLabel(), 'y1') + + self.plot.clear() + self.assertEqual(self.plot.getGraphXLabel(), 'XLabel') + self.assertEqual(self.plot.getGraphYLabel(), 'YLabel') + + +############################################################################## +# Log +############################################################################## + +class TestPlotEmptyLog(_PlotWidgetTest): + """Basic tests for log plot""" + def testEmptyPlotTitleLabelsLog(self): + self.plot.setGraphTitle('Empty Log Log') + self.plot.setGraphXLabel('X') + self.plot.setGraphYLabel('Y') + self.plot.setXAxisLogarithmic(True) + self.plot.setYAxisLogarithmic(True) + self.plot.resetZoom() + + +class TestPlotCurveLog(_PlotWidgetTest, ParametricTestCase): + """Basic tests for addCurve with log scale axes""" + + # Test data + xData = numpy.arange(1000) + 1 + yData = xData ** 2 + + def _setLabels(self): + self.plot.setGraphXLabel('X') + self.plot.setGraphYLabel('X * X') + + def testPlotCurveLogX(self): + self._setLabels() + self.plot.setXAxisLogarithmic(True) + self.plot.setGraphTitle('Curve X: Log Y: Linear') + + self.plot.addCurve(self.xData, self.yData, + legend="curve", + replace=False, resetzoom=True, + color='green', linestyle="-", symbol='o') + + def testPlotCurveLogY(self): + self._setLabels() + self.plot.setYAxisLogarithmic(True) + + self.plot.setGraphTitle('Curve X: Linear Y: Log') + + self.plot.addCurve(self.xData, self.yData, + legend="curve", + replace=False, resetzoom=True, + color='green', linestyle="-", symbol='o') + + def testPlotCurveLogXY(self): + self._setLabels() + self.plot.setXAxisLogarithmic(True) + self.plot.setYAxisLogarithmic(True) + + self.plot.setGraphTitle('Curve X: Log Y: Log') + + self.plot.addCurve(self.xData, self.yData, + legend="curve", + replace=False, resetzoom=True, + color='green', linestyle="-", symbol='o') + + def testPlotCurveErrorLogXY(self): + self.plot.setXAxisLogarithmic(True) + self.plot.setYAxisLogarithmic(True) + + # Every second error leads to negative number + errors = numpy.ones_like(self.xData) + errors[::2] = self.xData[::2] + 1 + + tests = [ # name, xerror, yerror + ('xerror=3', 3, None), + ('xerror=N array', errors, None), + ('xerror=Nx1 array', errors.reshape(len(errors), 1), None), + ('xerror=2xN array', numpy.array((errors, errors)), None), + ('yerror=6', None, 6), + ('yerror=N array', None, errors ** 2), + ('yerror=Nx1 array', None, (errors ** 2).reshape(len(errors), 1)), + ('yerror=2xN array', None, numpy.array((errors, errors)) ** 2), + ] + + for name, xError, yError in tests: + with self.subTest(name): + self.plot.setGraphTitle(name) + self.plot.addCurve(self.xData, self.yData, + legend=name, + xerror=xError, yerror=yError, + replace=False, resetzoom=True, + color='green', linestyle="-", symbol='o') + + self.qapp.processEvents() + + self.plot.clear() + self.plot.resetZoom() + self.qapp.processEvents() + + def testPlotCurveToggleLog(self): + """Add a curve with negative data and toggle log axis""" + arange = numpy.arange(1000) + 1 + tests = [ # name, xData, yData + ('x>0, some negative y', arange, arange - 500), + ('x>0, y<0', arange, -arange), + ('some negative x, y>0', arange - 500, arange), + ('x<0, y>0', -arange, arange), + ('some negative x and y', arange - 500, arange - 500), + ('x<0, y<0', -arange, -arange), + ] + + for name, xData, yData in tests: + with self.subTest(name): + self.plot.addCurve(xData, yData, resetzoom=True) + self.qapp.processEvents() + + # no log axis + xLim = self.plot.getGraphXLimits() + self.assertEqual(xLim, (min(xData), max(xData))) + yLim = self.plot.getGraphYLimits() + self.assertEqual(yLim, (min(yData), max(yData))) + + # x axis log + self.plot.setXAxisLogarithmic(True) + self.qapp.processEvents() + + xLim = self.plot.getGraphXLimits() + yLim = self.plot.getGraphYLimits() + positives = xData > 0 + if numpy.any(positives): + self.assertTrue(numpy.allclose( + xLim, (min(xData[positives]), max(xData[positives])))) + self.assertEqual( + yLim, (min(yData[positives]), max(yData[positives]))) + else: # No positive x in the curve + self.assertEqual(xLim, (1., 100.)) + self.assertEqual(yLim, (1., 100.)) + + # x axis and y axis log + self.plot.setYAxisLogarithmic(True) + self.qapp.processEvents() + + xLim = self.plot.getGraphXLimits() + yLim = self.plot.getGraphYLimits() + positives = numpy.logical_and(xData > 0, yData > 0) + if numpy.any(positives): + self.assertTrue(numpy.allclose( + xLim, (min(xData[positives]), max(xData[positives])))) + self.assertTrue(numpy.allclose( + yLim, (min(yData[positives]), max(yData[positives])))) + else: # No positive x and y in the curve + self.assertEqual(xLim, (1., 100.)) + self.assertEqual(yLim, (1., 100.)) + + # y axis log + self.plot.setXAxisLogarithmic(False) + self.qapp.processEvents() + + xLim = self.plot.getGraphXLimits() + yLim = self.plot.getGraphYLimits() + positives = yData > 0 + if numpy.any(positives): + self.assertEqual( + xLim, (min(xData[positives]), max(xData[positives]))) + self.assertTrue(numpy.allclose( + yLim, (min(yData[positives]), max(yData[positives])))) + else: # No positive y in the curve + self.assertEqual(xLim, (1., 100.)) + self.assertEqual(yLim, (1., 100.)) + + # no log axis + self.plot.setYAxisLogarithmic(False) + self.qapp.processEvents() + + xLim = self.plot.getGraphXLimits() + self.assertEqual(xLim, (min(xData), max(xData))) + yLim = self.plot.getGraphYLimits() + self.assertEqual(yLim, (min(yData), max(yData))) + + self.plot.clear() + self.plot.resetZoom() + self.qapp.processEvents() + + +class TestPlotImageLog(_PlotWidgetTest): + """Basic tests for addImage with log scale axes.""" + + def setUp(self): + super(TestPlotImageLog, self).setUp() + + self.plot.setGraphXLabel('Columns') + self.plot.setGraphYLabel('Rows') + + def testPlotColormapGrayLogX(self): + self.plot.setXAxisLogarithmic(True) + self.plot.setGraphTitle('CMap X: Log Y: Linear') + + colormap = {'name': 'gray', 'normalization': 'linear', + 'autoscale': True, 'vmin': 0.0, 'vmax': 1.0} + self.plot.addImage(DATA_2D, legend="image 1", + origin=(1., 1.), scale=(1., 1.), + replace=False, resetzoom=False, colormap=colormap) + self.plot.resetZoom() + + def testPlotColormapGrayLogY(self): + self.plot.setYAxisLogarithmic(True) + self.plot.setGraphTitle('CMap X: Linear Y: Log') + + colormap = {'name': 'gray', 'normalization': 'linear', + 'autoscale': True, 'vmin': 0.0, 'vmax': 1.0} + self.plot.addImage(DATA_2D, legend="image 1", + origin=(1., 1.), scale=(1., 1.), + replace=False, resetzoom=False, colormap=colormap) + self.plot.resetZoom() + + def testPlotColormapGrayLogXY(self): + self.plot.setXAxisLogarithmic(True) + self.plot.setYAxisLogarithmic(True) + self.plot.setGraphTitle('CMap X: Log Y: Log') + + colormap = {'name': 'gray', 'normalization': 'linear', + 'autoscale': True, 'vmin': 0.0, 'vmax': 1.0} + self.plot.addImage(DATA_2D, legend="image 1", + origin=(1., 1.), scale=(1., 1.), + replace=False, resetzoom=False, colormap=colormap) + self.plot.resetZoom() + + def testPlotRgbRgbaLogXY(self): + self.plot.setXAxisLogarithmic(True) + self.plot.setYAxisLogarithmic(True) + self.plot.setGraphTitle('RGB + RGBA X: Log Y: Log') + + rgb = numpy.array( + (((0, 0, 0), (128, 0, 0), (255, 0, 0)), + ((0, 128, 0), (0, 128, 128), (0, 128, 256))), + dtype=numpy.uint8) + + self.plot.addImage(rgb, legend="rgb", + origin=(1, 1), scale=(10, 10), + replace=False, resetzoom=False) + + rgba = numpy.array( + (((0, 0, 0, .5), (.5, 0, 0, 1), (1, 0, 0, .5)), + ((0, .5, 0, 1), (0, .5, .5, 1), (0, 1, 1, .5))), + dtype=numpy.float32) + + self.plot.addImage(rgba, legend="rgba", + origin=(5., 5.), scale=(10., 10.), + replace=False, resetzoom=False) + self.plot.resetZoom() + + +class TestPlotMarkerLog(_PlotWidgetTest): + """Basic tests for markers on log scales""" + + # Test marker parameters + markers = [ # x, y, color, selectable, draggable + (10., 10., 'blue', False, False), + (20., 20., 'red', False, False), + (40., 100., 'green', True, False), + (40., 500., 'gray', True, True), + (60., 800., 'black', False, True), + ] + + def setUp(self): + super(TestPlotMarkerLog, self).setUp() + + self.plot.setGraphYLabel('Rows') + self.plot.setGraphXLabel('Columns') + self.plot.setXAxisAutoScale(False) + self.plot.setYAxisAutoScale(False) + self.plot.setKeepDataAspectRatio(False) + self.plot.setLimits(1., 100., 1., 1000.) + self.plot.setXAxisLogarithmic(True) + self.plot.setYAxisLogarithmic(True) + + def testPlotMarkerXLog(self): + self.plot.setGraphTitle('Markers X, Log axes') + + for x, _, color, select, drag in self.markers: + name = str(x) + if select: + name += " sel." + if drag: + name += " drag" + self.plot.addXMarker(x, name, name, color, select, drag) + self.plot.resetZoom() + + def testPlotMarkerYLog(self): + self.plot.setGraphTitle('Markers Y, Log axes') + + for _, y, color, select, drag in self.markers: + name = str(y) + if select: + name += " sel." + if drag: + name += " drag" + self.plot.addYMarker(y, name, name, color, select, drag) + self.plot.resetZoom() + + def testPlotMarkerPtLog(self): + self.plot.setGraphTitle('Markers Pt, Log axes') + + for x, y, color, select, drag in self.markers: + name = "{0},{1}".format(x, y) + if select: + name += " sel." + if drag: + name += " drag" + self.plot.addMarker(x, y, name, name, color, select, drag) + self.plot.resetZoom() + + +class TestPlotItemLog(_PlotWidgetTest): + """Basic tests for items with log scale axes""" + + # Polygon coordinates and color + polygons = [ # legend, x coords, y coords, color + ('triangle', numpy.array((10, 30, 50)), + numpy.array((55, 70, 55)), 'red'), + ('square', numpy.array((10, 10, 50, 50)), + numpy.array((10, 50, 50, 10)), 'green'), + ('star', numpy.array((60, 70, 80, 60, 80)), + numpy.array((25, 50, 25, 40, 40)), 'blue'), + ] + + # Rectangle coordinantes and color + rectangles = [ # legend, x coords, y coords, color + ('square 1', numpy.array((1., 10.)), + numpy.array((1., 10.)), 'red'), + ('square 2', numpy.array((10., 20.)), + numpy.array((10., 20.)), 'green'), + ('square 3', numpy.array((20., 30.)), + numpy.array((20., 30.)), 'blue'), + ('rect 1', numpy.array((1., 30.)), + numpy.array((35., 40.)), 'black'), + ('line h', numpy.array((1., 30.)), + numpy.array((45., 45.)), 'darkRed'), + ] + + def setUp(self): + super(TestPlotItemLog, self).setUp() + + self.plot.setGraphYLabel('Rows') + self.plot.setGraphXLabel('Columns') + self.plot.setXAxisAutoScale(False) + self.plot.setYAxisAutoScale(False) + self.plot.setKeepDataAspectRatio(False) + self.plot.setLimits(1., 100., 1., 100.) + self.plot.setXAxisLogarithmic(True) + self.plot.setYAxisLogarithmic(True) + + def testPlotItemPolygonLogFill(self): + self.plot.setGraphTitle('Item Fill Log') + + for legend, xList, yList, color in self.polygons: + self.plot.addItem(xList, yList, legend=legend, + replace=False, + shape="polygon", fill=True, color=color) + self.plot.resetZoom() + + def testPlotItemPolygonLogNoFill(self): + self.plot.setGraphTitle('Item No Fill Log') + + for legend, xList, yList, color in self.polygons: + self.plot.addItem(xList, yList, legend=legend, + replace=False, + shape="polygon", fill=False, color=color) + self.plot.resetZoom() + + def testPlotItemRectangleLogFill(self): + self.plot.setGraphTitle('Rectangle Fill Log') + + for legend, xList, yList, color in self.rectangles: + self.plot.addItem(xList, yList, legend=legend, + replace=False, + shape="rectangle", fill=True, color=color) + self.plot.resetZoom() + + def testPlotItemRectangleLogNoFill(self): + self.plot.setGraphTitle('Rectangle No Fill Log') + + for legend, xList, yList, color in self.rectangles: + self.plot.addItem(xList, yList, legend=legend, + replace=False, + shape="rectangle", fill=False, color=color) + self.plot.resetZoom() + + +def suite(): + test_suite = unittest.TestSuite() + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestPlotWidget)) + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestPlotImage)) + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestPlotCurve)) + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestPlotMarker)) + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestPlotItem)) + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestPlotEmptyLog)) + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestPlotCurveLog)) + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestPlotImageLog)) + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestPlotMarkerLog)) + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestPlotItemLog)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/gui/plot/test/testPlotWindow.py b/silx/gui/plot/test/testPlotWindow.py new file mode 100644 index 0000000..5afd53a --- /dev/null +++ b/silx/gui/plot/test/testPlotWindow.py @@ -0,0 +1,138 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016 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. +# +# ###########################################################################*/ +"""Basic tests for PlotWindow""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "05/12/2016" + + +import doctest +import unittest + +from silx.gui.test.utils import TestCaseQt, getQToolButtonFromAction + +from silx.gui import qt +from silx.gui.plot import PlotWindow + + +# Test of the docstrings # + +# Makes sure a QApplication exists +_qapp = qt.QApplication.instance() or qt.QApplication([]) + + +def _tearDownQt(docTest): + """Tear down to use for test from docstring. + + Checks that plt widget is displayed + """ + _qapp.processEvents() + for obj in docTest.globs.values(): + if isinstance(obj, PlotWindow): + # Commented out as it takes too long + # qWaitForWindowExposedAndActivate(obj) + obj.setAttribute(qt.Qt.WA_DeleteOnClose) + obj.close() + del obj + + +plotWindowDocTestSuite = doctest.DocTestSuite('silx.gui.plot.PlotWindow', + tearDown=_tearDownQt) +"""Test suite of tests from the module's docstrings.""" + + +class TestPlotWindow(TestCaseQt): + """Base class for tests of PlotWindow.""" + + def setUp(self): + super(TestPlotWindow, self).setUp() + self.plot = PlotWindow() + self.plot.show() + self.qWaitForWindowExposed(self.plot) + + def tearDown(self): + self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) + self.plot.close() + del self.plot + super(TestPlotWindow, self).tearDown() + + def testActions(self): + """Test the actions QToolButtons""" + self.plot.setLimits(1, 100, 1, 100) + + checkList = [ # QAction, Plot state getter + (self.plot.xAxisAutoScaleAction, self.plot.isXAxisAutoScale), + (self.plot.yAxisAutoScaleAction, self.plot.isYAxisAutoScale), + (self.plot.xAxisLogarithmicAction, self.plot.isXAxisLogarithmic), + (self.plot.yAxisLogarithmicAction, self.plot.isYAxisLogarithmic), + (self.plot.gridAction, self.plot.getGraphGrid), + ] + + for action, getter in checkList: + self.mouseMove(self.plot) + initialState = getter() + toolButton = getQToolButtonFromAction(action) + self.assertIsNot(toolButton, None) + self.mouseClick(toolButton, qt.Qt.LeftButton) + self.assertNotEqual(getter(), initialState, + msg='"%s" state not changed' % action.text()) + + self.mouseClick(toolButton, qt.Qt.LeftButton) + self.assertEqual(getter(), initialState, + msg='"%s" state not changed' % action.text()) + + # Trigger a zoom reset + self.mouseMove(self.plot) + resetZoomAction = self.plot.resetZoomAction + toolButton = getQToolButtonFromAction(resetZoomAction) + self.assertIsNot(toolButton, None) + self.mouseClick(toolButton, qt.Qt.LeftButton) + + def testToolAspectRatio(self): + self.plot.toolBar() + self.plot.keepDataAspectRatioButton.keepDataAspectRatio() + self.assertTrue(self.plot.isKeepDataAspectRatio()) + self.plot.keepDataAspectRatioButton.dontKeepDataAspectRatio() + self.assertFalse(self.plot.isKeepDataAspectRatio()) + + def testToolYAxisOrigin(self): + self.plot.toolBar() + self.plot.yAxisInvertedButton.setYAxisUpward() + self.assertFalse(self.plot.isYAxisInverted()) + self.plot.yAxisInvertedButton.setYAxisDownward() + self.assertTrue(self.plot.isYAxisInverted()) + + +def suite(): + test_suite = unittest.TestSuite() + test_suite.addTest(plotWindowDocTestSuite) + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestPlotWindow)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/gui/plot/test/testProfile.py b/silx/gui/plot/test/testProfile.py new file mode 100644 index 0000000..43d3329 --- /dev/null +++ b/silx/gui/plot/test/testProfile.py @@ -0,0 +1,183 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-2017 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. +# +# ###########################################################################*/ +"""Basic tests for Profile""" + +__authors__ = ["T. Vincent", "P. Knobel"] +__license__ = "MIT" +__date__ = "23/02/2017" + +import numpy +import unittest + +from silx.test.utils import ParametricTestCase +from silx.gui.test.utils import ( + TestCaseQt, getQToolButtonFromAction) +from silx.gui import qt +from silx.gui.plot import PlotWindow, Plot1D, Plot2D, Profile +from silx.gui.plot.StackView import StackView + + +# Makes sure a QApplication exists +_qapp = qt.QApplication.instance() or qt.QApplication([]) + + +class TestProfileToolBar(TestCaseQt, ParametricTestCase): + """Tests for ProfileToolBar widget.""" + + def setUp(self): + super(TestProfileToolBar, self).setUp() + profileWindow = PlotWindow() + self.plot = PlotWindow() + self.toolBar = Profile.ProfileToolBar( + plot=self.plot, profileWindow=profileWindow) + self.plot.addToolBar(self.toolBar) + + self.plot.show() + self.qWaitForWindowExposed(self.plot) + profileWindow.show() + self.qWaitForWindowExposed(profileWindow) + + self.mouseMove(self.plot) # Move to center + self.qapp.processEvents() + + def tearDown(self): + self.qapp.processEvents() + self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) + self.plot.close() + del self.plot + del self.toolBar + + super(TestProfileToolBar, self).tearDown() + + def testAlignedProfile(self): + """Test horizontal and vertical profile, without and with image""" + # Use Plot backend widget to submit mouse events + widget = self.plot.getWidgetHandle() + + # 2 positions to use for mouse events + pos1 = widget.width() * 0.4, widget.height() * 0.4 + pos2 = widget.width() * 0.6, widget.height() * 0.6 + + for action in (self.toolBar.hLineAction, self.toolBar.vLineAction): + with self.subTest(mode=action.text()): + # Trigger tool button for mode + toolButton = getQToolButtonFromAction(action) + self.assertIsNot(toolButton, None) + self.mouseMove(toolButton) + self.mouseClick(toolButton, qt.Qt.LeftButton) + + # Without image + self.mouseMove(widget, pos=pos1) + self.mouseClick(widget, qt.Qt.LeftButton, pos=pos1) + + # with image + self.plot.addImage(numpy.arange(100 * 100).reshape(100, -1)) + self.mousePress(widget, qt.Qt.LeftButton, pos=pos1) + self.mouseMove(widget, pos=pos2) + self.mouseRelease(widget, qt.Qt.LeftButton, pos=pos2) + + self.mouseMove(widget) + self.mouseClick(widget, qt.Qt.LeftButton) + + def testDiagonalProfile(self): + """Test diagonal profile, without and with image""" + # Use Plot backend widget to submit mouse events + widget = self.plot.getWidgetHandle() + + # 2 positions to use for mouse events + pos1 = widget.width() * 0.4, widget.height() * 0.4 + pos2 = widget.width() * 0.6, widget.height() * 0.6 + + # Trigger tool button for diagonal profile mode + toolButton = getQToolButtonFromAction(self.toolBar.lineAction) + self.assertIsNot(toolButton, None) + self.mouseMove(toolButton) + self.mouseClick(toolButton, qt.Qt.LeftButton) + + for image in (False, True): + with self.subTest(image=image): + if image: + self.plot.addImage(numpy.arange(100 * 100).reshape(100, -1)) + + self.mouseMove(widget, pos=pos1) + self.mousePress(widget, qt.Qt.LeftButton, pos=pos1) + self.mouseMove(widget, pos=pos2) + self.mouseRelease(widget, qt.Qt.LeftButton, pos=pos2) + + self.plot.clear() + + +class TestGetProfilePlot(TestCaseQt): + + def testProfile1D(self): + plot = Plot2D() + plot.show() + self.qWaitForWindowExposed(plot) + plot.addImage([[0, 1], [2, 3]]) + self.assertIsInstance(plot.getProfileToolbar().getProfileMainWindow(), + qt.QMainWindow) + self.assertIsInstance(plot.getProfilePlot(), + Plot1D) + plot.setAttribute(qt.Qt.WA_DeleteOnClose) + plot.close() + del plot + + def testProfile2D(self): + """Test that the profile plot associated to a stack view is either a + Plot1D or a plot 2D instance.""" + plot = StackView() + plot.show() + self.qWaitForWindowExposed(plot) + + plot.setStack(numpy.array([[[0, 1], [2, 3]], + [[4, 5], [6, 7]]])) + + self.assertIsInstance(plot.getProfileToolbar().getProfileMainWindow(), + qt.QMainWindow) + + # plot.getProfileToolbar().profile3dAction.computeProfileIn2D() # default + + self.assertIsInstance(plot.getProfileToolbar().getProfilePlot(), + Plot2D) + plot.getProfileToolbar().profile3dAction.computeProfileIn1D() + self.assertIsInstance(plot.getProfileToolbar().getProfilePlot(), + Plot1D) + + plot.setAttribute(qt.Qt.WA_DeleteOnClose) + plot.close() + del plot + + +def suite(): + test_suite = unittest.TestSuite() + # test_suite.addTest(positionInfoTestSuite) + for testClass in (TestProfileToolBar, TestGetProfilePlot): + test_suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase( + testClass)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/gui/plot/test/testScatterMaskToolsWidget.py b/silx/gui/plot/test/testScatterMaskToolsWidget.py new file mode 100644 index 0000000..8b5f2ad --- /dev/null +++ b/silx/gui/plot/test/testScatterMaskToolsWidget.py @@ -0,0 +1,313 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-2017 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. +# +# ###########################################################################*/ +"""Basic tests for MaskToolsWidget""" + +__authors__ = ["T. Vincent", "P. Knobel"] +__license__ = "MIT" +__date__ = "10/07/2017" + + +import logging +import os.path +import unittest + +import numpy + +from silx.gui import qt +from silx.test.utils import temp_dir, ParametricTestCase +from silx.gui.test.utils import TestCaseQt, getQToolButtonFromAction +from silx.gui.plot import PlotWindow, ScatterMaskToolsWidget + +try: + import fabio +except ImportError: + fabio = None + + +logging.basicConfig() +_logger = logging.getLogger(__name__) + + +class TestScatterMaskToolsWidget(TestCaseQt, ParametricTestCase): + """Basic test for MaskToolsWidget""" + + def setUp(self): + super(TestScatterMaskToolsWidget, self).setUp() + self.plot = PlotWindow() + + self.widget = ScatterMaskToolsWidget.ScatterMaskToolsDockWidget( + plot=self.plot, name='TEST') + self.plot.addDockWidget(qt.Qt.BottomDockWidgetArea, self.widget) + + self.plot.show() + self.qWaitForWindowExposed(self.plot) + + self.maskWidget = self.widget.widget() + + def tearDown(self): + del self.maskWidget + del self.widget + + self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) + self.plot.close() + del self.plot + + super(TestScatterMaskToolsWidget, self).tearDown() + + def testEmptyPlot(self): + """Empty plot, display MaskToolsDockWidget, toggle multiple masks""" + self.maskWidget.setMultipleMasks('single') + self.qapp.processEvents() + + self.maskWidget.setMultipleMasks('exclusive') + self.qapp.processEvents() + + def _drag(self): + """Drag from plot center to offset position""" + plot = self.plot.centralWidget() + xCenter, yCenter = plot.width() // 2, plot.height() // 2 + offset = min(plot.width(), plot.height()) // 10 + + pos0 = xCenter, yCenter + pos1 = xCenter + offset, yCenter + offset + + self.mouseMove(plot, pos=pos0) + self.mousePress(plot, qt.Qt.LeftButton, pos=pos0) + self.mouseMove(plot, pos=pos1) + self.mouseRelease(plot, qt.Qt.LeftButton, pos=pos1) + + def _drawPolygon(self): + """Draw a star polygon in the plot""" + plot = self.plot.centralWidget() + x, y = plot.width() // 2, plot.height() // 2 + offset = min(plot.width(), plot.height()) // 10 + + star = [(x, y + offset), + (x - offset, y - offset), + (x + offset, y), + (x - offset, y), + (x + offset, y - offset)] + + for pos in star: + self.mouseMove(plot, pos=pos) + btn = qt.Qt.LeftButton if pos != star[-1] else qt.Qt.RightButton + self.mouseClick(plot, btn, pos=pos) + + def _drawPencil(self): + """Draw a star polygon in the plot""" + plot = self.plot.centralWidget() + x, y = plot.width() // 2, plot.height() // 2 + offset = min(plot.width(), plot.height()) // 10 + + star = [(x, y + offset), + (x - offset, y - offset), + (x + offset, y), + (x - offset, y), + (x + offset, y - offset)] + + self.mouseMove(plot, pos=star[0]) + self.mousePress(plot, qt.Qt.LeftButton, pos=star[0]) + for pos in star: + self.mouseMove(plot, pos=pos) + self.mouseRelease( + plot, qt.Qt.LeftButton, pos=star[-1]) + + def testWithAScatter(self): + """Plot with a Scatter: test MaskToolsWidget interactions""" + + # Add and remove a scatter (this should enable/disable GUI + change mask) + self.plot.addScatter( + x=numpy.arange(256), + y=numpy.arange(256), + value=numpy.random.random(256), + legend='test') + self.plot._setActiveItem(kind="scatter", legend="test") + self.qapp.processEvents() + + self.plot.remove('test', kind='scatter') + self.qapp.processEvents() + + self.plot.addScatter( + x=numpy.arange(1000), + y=1000 * (numpy.arange(1000) % 20), + value=numpy.random.random(1000), + legend='test') + self.plot._setActiveItem(kind="scatter", legend="test") + self.plot.resetZoom() + self.qapp.processEvents() + + # Test draw rectangle # + toolButton = getQToolButtonFromAction(self.maskWidget.rectAction) + self.assertIsNot(toolButton, None) + self.mouseClick(toolButton, qt.Qt.LeftButton) + + # mask + self.maskWidget.maskStateGroup.button(1).click() + self.qapp.processEvents() + self._drag() + + self.assertFalse( + numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))) + + # unmask same region + self.maskWidget.maskStateGroup.button(0).click() + self.qapp.processEvents() + self._drag() + self.assertTrue( + numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))) + + # Test draw polygon # + toolButton = getQToolButtonFromAction(self.maskWidget.polygonAction) + self.assertIsNot(toolButton, None) + self.mouseClick(toolButton, qt.Qt.LeftButton) + + # mask + self.maskWidget.maskStateGroup.button(1).click() + self.qapp.processEvents() + self._drawPolygon() + self.assertFalse( + numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))) + + # unmask same region + self.maskWidget.maskStateGroup.button(0).click() + self.qapp.processEvents() + self._drawPolygon() + self.assertTrue( + numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))) + + # Test draw pencil # + toolButton = getQToolButtonFromAction(self.maskWidget.pencilAction) + self.assertIsNot(toolButton, None) + self.mouseClick(toolButton, qt.Qt.LeftButton) + + self.maskWidget.pencilSpinBox.setValue(10) + self.qapp.processEvents() + + # mask + self.maskWidget.maskStateGroup.button(1).click() + self.qapp.processEvents() + self._drawPencil() + self.assertFalse( + numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))) + + # unmask same region + self.maskWidget.maskStateGroup.button(0).click() + self.qapp.processEvents() + self._drawPencil() + self.assertTrue( + numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))) + + # Test no draw tool # + toolButton = getQToolButtonFromAction(self.maskWidget.browseAction) + self.assertIsNot(toolButton, None) + self.mouseClick(toolButton, qt.Qt.LeftButton) + + self.plot.clear() + + def __loadSave(self, file_format): + self.plot.addScatter( + x=numpy.arange(256), + y=25 * (numpy.arange(256) % 10), + value=numpy.random.random(256), + legend='test') + self.plot._setActiveItem(kind="scatter", legend="test") + self.plot.resetZoom() + self.qapp.processEvents() + + # Draw a polygon mask + toolButton = getQToolButtonFromAction(self.maskWidget.polygonAction) + self.assertIsNot(toolButton, None) + self.mouseClick(toolButton, qt.Qt.LeftButton) + self._drawPolygon() + + ref_mask = self.maskWidget.getSelectionMask() + self.assertFalse(numpy.all(numpy.equal(ref_mask, 0))) + + with temp_dir() as tmp: + mask_filename = os.path.join(tmp, 'mask.' + file_format) + self.maskWidget.save(mask_filename, file_format) + + self.maskWidget.resetSelectionMask() + self.assertTrue( + numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))) + + self.maskWidget.load(mask_filename) + self.assertTrue(numpy.all(numpy.equal( + self.maskWidget.getSelectionMask(), ref_mask))) + + def testLoadSaveNpy(self): + self.__loadSave("npy") + + def testLoadSaveCsv(self): + self.__loadSave("csv") + + def testSigMaskChangedEmitted(self): + self.qapp.processEvents() + self.plot.addScatter( + x=numpy.arange(1000), + y=1000 * (numpy.arange(1000) % 20), + value=numpy.ones((1000,)), + legend='test') + self.plot._setActiveItem(kind="scatter", legend="test") + self.plot.resetZoom() + self.qapp.processEvents() + + self.plot.remove('test', kind='scatter') + self.qapp.processEvents() + + self.plot.addScatter( + x=numpy.arange(1000), + y=1000 * (numpy.arange(1000) % 20), + value=numpy.random.random(1000), + legend='test') + + l = [] + + def slot(): + l.append(1) + + self.maskWidget.sigMaskChanged.connect(slot) + + # rectangle mask + toolButton = getQToolButtonFromAction(self.maskWidget.rectAction) + self.assertIsNot(toolButton, None) + self.mouseClick(toolButton, qt.Qt.LeftButton) + self.maskWidget.maskStateGroup.button(1).click() + self.qapp.processEvents() + self._drag() + + self.assertGreater(len(l), 0) + + +def suite(): + test_suite = unittest.TestSuite() + for TestClass in (TestScatterMaskToolsWidget,): + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestClass)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/gui/plot/test/testStackView.py b/silx/gui/plot/test/testStackView.py new file mode 100644 index 0000000..69584cd --- /dev/null +++ b/silx/gui/plot/test/testStackView.py @@ -0,0 +1,209 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-2017 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. +# +# ###########################################################################*/ +"""Basic tests for StackView""" + +__authors__ = ["P. Knobel"] +__license__ = "MIT" +__date__ = "20/03/2017" + + +import unittest +import numpy + +from silx.gui.test.utils import TestCaseQt + +from silx.gui import qt +from silx.gui.plot import StackView +from silx.gui.plot.StackView import StackViewMainWindow + +from silx.utils.array_like import ListOfImages + + +# Makes sure a QApplication exists +_qapp = qt.QApplication.instance() or qt.QApplication([]) + + +class TestStackView(TestCaseQt): + """Base class for tests of StackView.""" + + def setUp(self): + super(TestStackView, self).setUp() + self.stackview = StackView() + self.stackview.show() + self.qWaitForWindowExposed(self.stackview) + self.mystack = numpy.fromfunction( + lambda i, j, k: numpy.sin(i/15.) + numpy.cos(j/4.) + 2 * numpy.sin(k/6.), + (10, 20, 30) + ) + + def tearDown(self): + self.stackview.setAttribute(qt.Qt.WA_DeleteOnClose) + self.stackview.close() + del self.stackview + super(TestStackView, self).tearDown() + + def testSetStack(self): + self.stackview.setStack(self.mystack) + self.stackview.setColormap("viridis", autoscale=True) + my_trans_stack, params = self.stackview.getStack() + self.assertEqual(my_trans_stack.shape, self.mystack.shape) + self.assertTrue(numpy.array_equal(self.mystack, + my_trans_stack)) + self.assertEqual(params["colormap"]["name"], + "viridis") + + def testSetStackPerspective(self): + self.stackview.setStack(self.mystack, perspective=1) + # my_orig_stack, params = self.stackview.getStack() + my_trans_stack, params = self.stackview.getCurrentView() + + # get stack returns the transposed data, depending on the perspective + self.assertEqual(my_trans_stack.shape, + (self.mystack.shape[1], self.mystack.shape[0], self.mystack.shape[2])) + self.assertTrue(numpy.array_equal(numpy.transpose(self.mystack, axes=(1, 0, 2)), + my_trans_stack)) + + def testSetStackListOfImages(self): + loi = [self.mystack[i] for i in range(self.mystack.shape[0])] + + self.stackview.setStack(loi) + my_orig_stack, params = self.stackview.getStack(returnNumpyArray=True) + my_trans_stack, params = self.stackview.getStack(returnNumpyArray=True) + self.assertEqual(my_trans_stack.shape, self.mystack.shape) + self.assertTrue(numpy.array_equal(self.mystack, + my_trans_stack)) + self.assertTrue(numpy.array_equal(self.mystack, + my_orig_stack)) + self.assertIsInstance(my_trans_stack, numpy.ndarray) + + self.stackview.setStack(loi, perspective=2) + my_orig_stack, params = self.stackview.getStack(copy=False) + my_trans_stack, params = self.stackview.getCurrentView(copy=False) + # getStack(copy=False) must return the object set in setStack + self.assertIs(my_orig_stack, loi) + # getCurrentView(copy=False) returns a ListOfImages whose .images + # attr is the original data + self.assertEqual(my_trans_stack.shape, + (self.mystack.shape[2], self.mystack.shape[0], self.mystack.shape[1])) + self.assertTrue(numpy.array_equal(numpy.array(my_trans_stack), + numpy.transpose(self.mystack, axes=(2, 0, 1)))) + self.assertIsInstance(my_trans_stack, + ListOfImages) # returnNumpyArray=False by default in getStack + self.assertIs(my_trans_stack.images, loi) + + def testPerspective(self): + self.stackview.setStack(numpy.arange(24).reshape((2, 3, 4))) + self.assertEqual(self.stackview._perspective, 0, + "Default perspective is not 0 (dim1-dim2).") + + self.stackview._StackView__planeSelection.setPerspective(1) + self.assertEqual(self.stackview._perspective, 1, + "Plane selection combobox not updating perspective") + + self.stackview.setStack(numpy.arange(6).reshape((1, 2, 3))) + self.assertEqual(self.stackview._perspective, 0, + "Default perspective not restored in setStack.") + + self.stackview.setStack(numpy.arange(24).reshape((2, 3, 4)), perspective=2) + self.assertEqual(self.stackview._perspective, 2, + "Perspective not set in setStack(..., perspective=2).") + + def testTitle(self): + """Test that the plot title contains the proper Z information""" + self.stackview.setStack(numpy.arange(24).reshape((4, 3, 2)), + calibrations=[(0, 1), (-10, 10), (3.14, 3.14)]) + self.assertEqual(self.stackview._plot.getGraphTitle(), + "Image z=0") + self.stackview.setFrameNumber(2) + self.assertEqual(self.stackview._plot.getGraphTitle(), + "Image z=2") + + self.stackview._StackView__planeSelection.setPerspective(1) + self.stackview.setFrameNumber(0) + self.assertEqual(self.stackview._plot.getGraphTitle(), + "Image z=-10") + self.stackview.setFrameNumber(2) + self.assertEqual(self.stackview._plot.getGraphTitle(), + "Image z=10") + + self.stackview._StackView__planeSelection.setPerspective(2) + self.stackview.setFrameNumber(0) + self.assertEqual(self.stackview._plot.getGraphTitle(), + "Image z=3.14") + self.stackview.setFrameNumber(1) + self.assertEqual(self.stackview._plot.getGraphTitle(), + "Image z=6.28") + + +class TestStackViewMainWindow(TestCaseQt): + """Base class for tests of StackView.""" + + def setUp(self): + super(TestStackViewMainWindow, self).setUp() + self.stackview = StackViewMainWindow() + self.stackview.show() + self.qWaitForWindowExposed(self.stackview) + self.mystack = numpy.fromfunction( + lambda i, j, k: numpy.sin(i/15.) + numpy.cos(j/4.) + 2 * numpy.sin(k/6.), + (10, 20, 30) + ) + + def tearDown(self): + self.stackview.setAttribute(qt.Qt.WA_DeleteOnClose) + self.stackview.close() + del self.stackview + super(TestStackViewMainWindow, self).tearDown() + + def testSetStack(self): + self.stackview.setStack(self.mystack) + self.stackview.setColormap("viridis", autoscale=True) + my_trans_stack, params = self.stackview.getStack() + self.assertEqual(my_trans_stack.shape, self.mystack.shape) + self.assertTrue(numpy.array_equal(self.mystack, + my_trans_stack)) + self.assertEqual(params["colormap"]["name"], + "viridis") + + def testSetStackPerspective(self): + self.stackview.setStack(self.mystack, perspective=1) + my_trans_stack, params = self.stackview.getCurrentView() + # get stack returns the transposed data, depending on the perspective + self.assertEqual(my_trans_stack.shape, + (self.mystack.shape[1], self.mystack.shape[0], self.mystack.shape[2])) + self.assertTrue(numpy.array_equal(numpy.transpose(self.mystack, axes=(1, 0, 2)), + my_trans_stack)) + + +def suite(): + test_suite = unittest.TestSuite() + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestStackView)) + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestStackViewMainWindow)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') |