summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.rst152
-rw-r--r--PKG-INFO12
-rw-r--r--README.rst9
-rwxr-xr-xbuild-deb.sh3
-rw-r--r--debian/changelog35
-rw-r--r--debian/compat1
-rw-r--r--debian/control105
-rw-r--r--debian/gitlab-ci.yml20
-rw-r--r--debian/gitlab-ci.yml.tpl3
-rw-r--r--debian/patches/0002-use-the-system-mathjax-privacy-breach.patch4
-rw-r--r--debian/patches/0003-do-not-modify-PYTHONPATH-from-setup.py.patch4
-rw-r--r--debian/patches/0003-fix-unit-test.patch2
-rw-r--r--debian/patches/0004-fix-FTBFS-with-numpy-0.16.patch24
-rw-r--r--debian/patches/0006-Tests-if-openCL-can-be-used.patch28
-rw-r--r--debian/patches/series2
-rw-r--r--debian/pydist-overrides3
-rw-r--r--debian/python-silx-doc.doc-base6
-rwxr-xr-xdebian/rules12
-rw-r--r--debian/tests/control15
-rw-r--r--debian/tests/control.autodep831
-rw-r--r--doc/source/conf.py3
-rw-r--r--doc/source/ext/snapshotqt_directive.py242
-rw-r--r--doc/source/install.rst4
-rw-r--r--doc/source/modules/gui/data/img/ArrayTableWidget.pngbin29088 -> 21097 bytes
-rw-r--r--doc/source/modules/gui/data/img/DataViewer.pngbin37627 -> 20225 bytes
-rw-r--r--doc/source/modules/gui/gallery.rst115
-rw-r--r--doc/source/modules/gui/icons.rst9
-rw-r--r--doc/source/modules/gui/plot/dev.rst1
-rw-r--r--doc/source/modules/gui/plot/img/BasicGridStatsWidget.pngbin0 -> 5702 bytes
-rw-r--r--doc/source/modules/gui/plot/img/BasicStatsWidget.pngbin0 -> 6575 bytes
-rw-r--r--doc/source/modules/gui/plot/img/LimitsToolBar.pngbin2331 -> 21499 bytes
-rw-r--r--doc/source/modules/gui/plot/img/logColorbar.pngbin8575 -> 5240 bytes
-rw-r--r--doc/source/modules/gui/plot/index.rst7
-rw-r--r--doc/source/modules/gui/plot/roi.rst31
-rw-r--r--doc/source/modules/gui/plot/statswidget.rst16
-rw-r--r--doc/source/modules/gui/plot/utils.rst12
-rw-r--r--doc/source/modules/gui/plot3d/glutils.rst10
-rw-r--r--doc/source/modules/gui/plot3d/img/SceneWidget.pngbin349485 -> 65009 bytes
-rw-r--r--doc/source/modules/gui/plot3d/items.rst4
-rw-r--r--doc/source/modules/gui/widgets/img/FrameBrowser.pngbin3731 -> 2161 bytes
-rw-r--r--doc/source/modules/gui/widgets/img/HorizontalSliderWithBrowser.pngbin4215 -> 2278 bytes
-rw-r--r--doc/source/modules/gui/widgets/img/PeriodicCombo.pngbin3464 -> 1878 bytes
-rw-r--r--doc/source/modules/gui/widgets/img/PeriodicList.pngbin12035 -> 17621 bytes
-rw-r--r--doc/source/modules/gui/widgets/img/PeriodicTable.pngbin35124 -> 25540 bytes
-rw-r--r--doc/source/modules/gui/widgets/img/RangeSlider.pngbin2686 -> 1024 bytes
-rw-r--r--doc/source/modules/gui/widgets/img/TableWidget.pngbin4058 -> 3156 bytes
-rw-r--r--doc/source/modules/gui/widgets/img/ThreadPoolPushButton.pngbin2729 -> 1577 bytes
-rw-r--r--doc/source/modules/gui/widgets/img/WaitingPushButton.pngbin1962 -> 941 bytes
-rw-r--r--doc/source/modules/image/index.rst2
-rw-r--r--doc/source/modules/image/shapes.rst4
-rw-r--r--doc/source/modules/opencl/convolution.rst10
-rw-r--r--doc/source/modules/opencl/index.rst4
-rw-r--r--doc/source/modules/opencl/processing.rst10
-rw-r--r--doc/source/modules/opencl/sinofilter.rst9
-rw-r--r--doc/source/modules/opencl/statistics.rst10
-rw-r--r--doc/source/sample_code/img/plot3dUpdateScatterFromThread.pngbin0 -> 181775 bytes
-rw-r--r--doc/source/sample_code/index.rst91
-rw-r--r--examples/compareImages.py110
-rw-r--r--examples/customDataView.py4
-rw-r--r--examples/dropZones.py134
-rw-r--r--examples/fileDialog.py4
-rwxr-xr-xexamples/hdf5widget.py40
-rwxr-xr-xexamples/imageview.py5
-rw-r--r--examples/plot3dSceneWindow.py15
-rw-r--r--examples/plot3dUpdateScatterFromThread.py176
-rw-r--r--examples/plotInteractiveImageROI.py18
-rw-r--r--examples/plotLimits.py6
-rw-r--r--examples/plotStats.py67
-rw-r--r--examples/plotWidget.py72
-rwxr-xr-xexamples/printPreview.py24
-rw-r--r--examples/syncPlotLocation.py105
-rw-r--r--examples/viewer3DVolume.py10
-rw-r--r--package/debian8/control87
-rwxr-xr-xpackage/debian8/rules4
-rw-r--r--package/debian9/control8
-rw-r--r--requirements-dev.txt3
-rw-r--r--requirements.txt17
-rwxr-xr-xrun_tests.py9
-rw-r--r--setup.py82
-rw-r--r--silx.egg-info/PKG-INFO12
-rw-r--r--silx.egg-info/SOURCES.txt67
-rw-r--r--silx.egg-info/requires.txt3
-rw-r--r--silx/_config.py46
-rw-r--r--silx/app/convert.py48
-rw-r--r--silx/app/test/test_convert.py16
-rw-r--r--silx/app/view/About.py5
-rw-r--r--silx/app/view/Viewer.py104
-rw-r--r--silx/app/view/main.py74
-rw-r--r--silx/app/view/test/test_view.py38
-rw-r--r--silx/gui/_glutils/Context.py42
-rw-r--r--silx/gui/_glutils/Program.py12
-rw-r--r--silx/gui/_glutils/Texture.py10
-rw-r--r--silx/gui/_glutils/__init__.py5
-rw-r--r--silx/gui/_glutils/utils.py73
-rw-r--r--silx/gui/colors.py460
-rw-r--r--silx/gui/console.py32
-rw-r--r--silx/gui/data/ArrayTableModel.py5
-rw-r--r--silx/gui/data/DataViewer.py92
-rw-r--r--silx/gui/data/DataViewerFrame.py5
-rw-r--r--silx/gui/data/DataViewerSelector.py6
-rw-r--r--silx/gui/data/DataViews.py545
-rw-r--r--silx/gui/data/Hdf5TableView.py13
-rw-r--r--silx/gui/data/HexaTableView.py8
-rw-r--r--silx/gui/data/NXdataWidgets.py401
-rw-r--r--silx/gui/data/TextFormatter.py50
-rw-r--r--silx/gui/data/_VolumeWindow.py148
-rw-r--r--silx/gui/data/test/test_arraywidget.py6
-rw-r--r--silx/gui/data/test/test_dataviewer.py20
-rw-r--r--silx/gui/data/test/test_numpyaxesselector.py7
-rw-r--r--silx/gui/data/test/test_textformatter.py14
-rw-r--r--silx/gui/dialog/AbstractDataFileDialog.py88
-rw-r--r--silx/gui/dialog/ColormapDialog.py358
-rw-r--r--silx/gui/dialog/DataFileDialog.py10
-rw-r--r--silx/gui/dialog/FileTypeComboBox.py39
-rw-r--r--silx/gui/dialog/ImageFileDialog.py28
-rw-r--r--silx/gui/dialog/SafeFileSystemModel.py6
-rw-r--r--silx/gui/dialog/test/test_colormapdialog.py42
-rw-r--r--silx/gui/dialog/test/test_datafiledialog.py130
-rw-r--r--silx/gui/dialog/test/test_imagefiledialog.py135
-rw-r--r--silx/gui/dialog/utils.py6
-rw-r--r--silx/gui/hdf5/Hdf5Formatter.py17
-rw-r--r--silx/gui/hdf5/Hdf5Item.py38
-rw-r--r--silx/gui/hdf5/Hdf5TreeModel.py41
-rw-r--r--silx/gui/hdf5/NexusSortFilterProxyModel.py4
-rw-r--r--silx/gui/hdf5/_utils.py74
-rw-r--r--silx/gui/hdf5/test/test_hdf5.py67
-rw-r--r--silx/gui/icons.py8
-rw-r--r--silx/gui/plot/ColorBar.py19
-rw-r--r--silx/gui/plot/CompareImages.py79
-rw-r--r--silx/gui/plot/ComplexImageView.py90
-rw-r--r--silx/gui/plot/CurvesROIWidget.py1866
-rw-r--r--silx/gui/plot/MaskToolsWidget.py108
-rw-r--r--silx/gui/plot/PlotInteraction.py148
-rw-r--r--silx/gui/plot/PlotToolButtons.py133
-rw-r--r--silx/gui/plot/PlotWidget.py570
-rw-r--r--silx/gui/plot/PlotWindow.py137
-rw-r--r--silx/gui/plot/PrintPreviewToolButton.py61
-rw-r--r--silx/gui/plot/Profile.py138
-rw-r--r--silx/gui/plot/ProfileMainWindow.py14
-rw-r--r--silx/gui/plot/ScatterMaskToolsWidget.py65
-rw-r--r--silx/gui/plot/ScatterView.py76
-rw-r--r--silx/gui/plot/StackView.py10
-rw-r--r--silx/gui/plot/StatsWidget.py1910
-rw-r--r--silx/gui/plot/_BaseMaskToolsWidget.py197
-rw-r--r--silx/gui/plot/_utils/delaunay.py (renamed from silx/third_party/concurrent_futures.py)53
-rw-r--r--silx/gui/plot/_utils/dtime_ticklayout.py4
-rw-r--r--silx/gui/plot/actions/control.py21
-rw-r--r--silx/gui/plot/actions/io.py38
-rw-r--r--silx/gui/plot/backends/BackendBase.py54
-rw-r--r--silx/gui/plot/backends/BackendMatplotlib.py275
-rw-r--r--silx/gui/plot/backends/BackendOpenGL.py596
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotCurve.py119
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotFrame.py124
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotTriangles.py193
-rw-r--r--silx/gui/plot/backends/glutils/GLSupport.py63
-rw-r--r--silx/gui/plot/backends/glutils/GLText.py23
-rw-r--r--silx/gui/plot/backends/glutils/GLTexture.py3
-rw-r--r--silx/gui/plot/backends/glutils/__init__.py3
-rw-r--r--silx/gui/plot/items/__init__.py9
-rw-r--r--silx/gui/plot/items/axis.py6
-rw-r--r--silx/gui/plot/items/complex.py127
-rw-r--r--silx/gui/plot/items/core.py234
-rw-r--r--silx/gui/plot/items/curve.py11
-rw-r--r--silx/gui/plot/items/histogram.py6
-rw-r--r--silx/gui/plot/items/image.py72
-rw-r--r--silx/gui/plot/items/marker.py17
-rw-r--r--silx/gui/plot/items/roi.py112
-rw-r--r--silx/gui/plot/items/scatter.py234
-rw-r--r--silx/gui/plot/items/shape.py45
-rw-r--r--silx/gui/plot/matplotlib/Colormap.py16
-rw-r--r--silx/gui/plot/matplotlib/__init__.py76
-rw-r--r--silx/gui/plot/stats/stats.py400
-rw-r--r--silx/gui/plot/stats/statshandler.py124
-rw-r--r--silx/gui/plot/test/testAlphaSlider.py5
-rw-r--r--silx/gui/plot/test/testComplexImageView.py6
-rw-r--r--silx/gui/plot/test/testCurvesROIWidget.py349
-rw-r--r--silx/gui/plot/test/testItem.py13
-rw-r--r--silx/gui/plot/test/testMaskToolsWidget.py7
-rw-r--r--silx/gui/plot/test/testPlotWidget.py116
-rw-r--r--silx/gui/plot/test/testPlotWindow.py29
-rw-r--r--silx/gui/plot/test/testProfile.py6
-rw-r--r--silx/gui/plot/test/testSaveAction.py20
-rw-r--r--silx/gui/plot/test/testScatterMaskToolsWidget.py5
-rw-r--r--silx/gui/plot/test/testStackView.py6
-rw-r--r--silx/gui/plot/test/testStats.py491
-rw-r--r--silx/gui/plot/test/testUtilsAxis.py49
-rw-r--r--silx/gui/plot/tools/profile/ScatterProfileToolBar.py362
-rw-r--r--silx/gui/plot/tools/roi.py41
-rw-r--r--silx/gui/plot/tools/test/testScatterProfileToolBar.py3
-rw-r--r--silx/gui/plot/tools/test/testTools.py30
-rw-r--r--silx/gui/plot/tools/toolbars.py28
-rw-r--r--silx/gui/plot/utils/axis.py288
-rw-r--r--silx/gui/plot3d/ParamTreeView.py2
-rw-r--r--silx/gui/plot3d/Plot3DWidget.py52
-rw-r--r--silx/gui/plot3d/Plot3DWindow.py21
-rw-r--r--silx/gui/plot3d/ScalarFieldView.py21
-rw-r--r--silx/gui/plot3d/SceneWidget.py55
-rw-r--r--silx/gui/plot3d/SceneWindow.py22
-rw-r--r--silx/gui/plot3d/_model/items.py575
-rw-r--r--silx/gui/plot3d/actions/mode.py61
-rw-r--r--silx/gui/plot3d/items/__init__.py8
-rw-r--r--silx/gui/plot3d/items/core.py4
-rw-r--r--silx/gui/plot3d/items/mesh.py288
-rw-r--r--silx/gui/plot3d/items/mixins.py39
-rw-r--r--silx/gui/plot3d/items/scatter.py143
-rw-r--r--silx/gui/plot3d/items/volume.py310
-rw-r--r--silx/gui/plot3d/scene/camera.py2
-rw-r--r--silx/gui/plot3d/scene/core.py11
-rw-r--r--silx/gui/plot3d/scene/cutplane.py18
-rw-r--r--silx/gui/plot3d/scene/function.py87
-rw-r--r--silx/gui/plot3d/scene/interaction.py60
-rw-r--r--silx/gui/plot3d/scene/primitives.py138
-rw-r--r--silx/gui/plot3d/scene/utils.py73
-rw-r--r--silx/gui/plot3d/scene/viewport.py75
-rw-r--r--silx/gui/plot3d/test/__init__.py8
-rw-r--r--silx/gui/plot3d/test/testSceneWidget.py84
-rw-r--r--silx/gui/plot3d/test/testSceneWidgetPicking.py149
-rw-r--r--silx/gui/plot3d/test/testSceneWindow.py209
-rw-r--r--silx/gui/plot3d/test/testStatsWidget.py213
-rw-r--r--silx/gui/plot3d/tools/PositionInfoWidget.py42
-rw-r--r--silx/gui/qt/_pyside_dynamic.py54
-rw-r--r--silx/gui/qt/_qt.py30
-rw-r--r--silx/gui/qt/inspect.py13
-rw-r--r--silx/gui/test/test_colors.py112
-rw-r--r--silx/gui/utils/concurrent.py4
-rw-r--r--silx/gui/utils/projecturl.py77
-rw-r--r--silx/gui/utils/test/test_async.py4
-rw-r--r--silx/gui/utils/testutils.py33
-rw-r--r--silx/gui/widgets/PrintPreview.py74
-rw-r--r--silx/gui/widgets/RangeSlider.py198
-rw-r--r--silx/gui/widgets/UrlSelectionTable.py164
-rw-r--r--silx/image/bilinear.c3007
-rw-r--r--silx/image/marchingsquares/_mergeimpl.cpp3488
-rw-r--r--silx/image/shapes.c3833
-rw-r--r--silx/image/shapes.pyx26
-rw-r--r--silx/image/test/test_shapes.py44
-rw-r--r--silx/image/tomography.py170
-rw-r--r--silx/io/commonh5.py13
-rw-r--r--silx/io/convert.py29
-rw-r--r--silx/io/dictdump.py20
-rw-r--r--silx/io/fabioh5.py61
-rw-r--r--silx/io/nxdata/__init__.py4
-rw-r--r--silx/io/nxdata/_utils.py5
-rw-r--r--silx/io/nxdata/parse.py93
-rw-r--r--silx/io/nxdata/write.py5
-rw-r--r--silx/io/octaveh5.py13
-rw-r--r--silx/io/specfile.c4996
-rw-r--r--silx/io/specfile.pyx10
-rw-r--r--silx/io/specfile/src/locale_management.c3
-rw-r--r--silx/io/specfile/src/sfdata.c27
-rw-r--r--silx/io/specfile/src/sftools.c3
-rw-r--r--silx/io/spech5.py5
-rw-r--r--silx/io/test/test_commonh5.py61
-rw-r--r--silx/io/test/test_dictdump.py9
-rw-r--r--silx/io/test/test_fabioh5.py42
-rw-r--r--silx/io/test/test_nxdata.py13
-rw-r--r--silx/io/test/test_specfile.py13
-rw-r--r--silx/io/test/test_spech5.py20
-rw-r--r--silx/io/test/test_spectoh5.py17
-rw-r--r--silx/io/test/test_utils.py267
-rw-r--r--silx/io/url.py7
-rw-r--r--silx/io/utils.py141
-rw-r--r--silx/math/chistogramnd.c3529
-rw-r--r--silx/math/chistogramnd.pyx2
-rw-r--r--silx/math/chistogramnd_lut.c4089
-rw-r--r--silx/math/colormap.c3943
-rw-r--r--silx/math/colormap.pyx6
-rw-r--r--silx/math/combo.c3092
-rw-r--r--silx/math/combo.pyx4
-rw-r--r--silx/math/fft/__init__.py8
-rw-r--r--silx/math/fft/basefft.py149
-rw-r--r--silx/math/fft/clfft.py284
-rw-r--r--silx/math/fft/cufft.py253
-rw-r--r--silx/math/fft/fft.py96
-rw-r--r--silx/math/fft/fftw.py210
-rw-r--r--silx/math/fft/npfft.py124
-rw-r--r--silx/math/fft/setup.py (renamed from silx/third_party/six.py)36
-rw-r--r--silx/math/fft/test/__init__.py (renamed from silx/sx/test/__init__.py)17
-rw-r--r--silx/math/fft/test/test_fft.py265
-rw-r--r--silx/math/fit/bgtheories.py4
-rw-r--r--silx/math/fit/filters.c5709
-rw-r--r--silx/math/fit/filters.pyx35
-rw-r--r--silx/math/fit/filters/include/filters.h2
-rw-r--r--silx/math/fit/functions.c5882
-rw-r--r--silx/math/fit/functions.pyx4
-rw-r--r--silx/math/fit/peaks.c3255
-rw-r--r--silx/math/fit/peaks.pyx4
-rw-r--r--silx/math/marchingcubes.cpp3442
-rw-r--r--silx/math/marchingcubes.pyx2
-rw-r--r--silx/math/medianfilter/include/median_filter.hpp20
-rw-r--r--silx/math/medianfilter/medianfilter.cpp3533
-rw-r--r--silx/math/medianfilter/medianfilter.pyx2
-rw-r--r--silx/math/medianfilter/test/benchmark.py4
-rw-r--r--silx/math/setup.py1
-rw-r--r--silx/math/test/__init__.py2
-rw-r--r--silx/math/test/test_HistogramndLut_nominal.py3
-rw-r--r--silx/math/test/test_colormap.py8
-rw-r--r--silx/math/test/test_combo.py21
-rw-r--r--silx/math/test/test_histogramnd_nominal.py27
-rw-r--r--silx/opencl/backprojection.py470
-rw-r--r--silx/opencl/codec/test/test_byte_offset.py9
-rw-r--r--silx/opencl/common.py61
-rw-r--r--silx/opencl/convolution.py555
-rw-r--r--silx/opencl/processing.py85
-rw-r--r--silx/opencl/sinofilter.py436
-rw-r--r--silx/opencl/sparse.py331
-rw-r--r--silx/opencl/statistics.py224
-rw-r--r--silx/opencl/test/__init__.py11
-rw-r--r--silx/opencl/test/test_backprojection.py97
-rw-r--r--silx/opencl/test/test_convolution.py263
-rw-r--r--silx/opencl/test/test_kahan.py269
-rw-r--r--silx/opencl/test/test_sparse.py192
-rw-r--r--silx/opencl/test/test_stats.py114
-rw-r--r--silx/resources/__init__.py178
-rw-r--r--silx/resources/gui/icons/compare-mode-a-minus-b.pngbin0 -> 3862 bytes
-rw-r--r--silx/resources/gui/icons/compare-mode-a-minus-b.svg25
-rw-r--r--silx/resources/gui/icons/eye.pngbin0 -> 755 bytes
-rw-r--r--silx/resources/gui/icons/eye.svg23
-rw-r--r--silx/resources/gui/icons/pointing-hand.pngbin0 -> 680 bytes
-rw-r--r--silx/resources/gui/icons/pointing-hand.svg2
-rw-r--r--[-rwxr-xr-x]silx/resources/gui/icons/shape-ellipse.pngbin743 -> 643 bytes
-rw-r--r--silx/resources/gui/icons/shape-ellipse.svg7
-rw-r--r--silx/resources/opencl/array_utils.cl66
-rw-r--r--silx/resources/opencl/convolution.cl312
-rw-r--r--silx/resources/opencl/convolution_textures.cl374
-rw-r--r--silx/resources/opencl/kahan.cl143
-rw-r--r--silx/resources/opencl/sparse.cl84
-rw-r--r--silx/resources/opencl/statistics.cl208
-rw-r--r--silx/setup.py3
-rw-r--r--silx/sx/__init__.py15
-rw-r--r--silx/sx/_plot.py48
-rw-r--r--silx/sx/_plot3d.py14
-rw-r--r--silx/test/__init__.py7
-rw-r--r--silx/test/test_resources.py58
-rw-r--r--silx/test/test_sx.py (renamed from silx/sx/test/test_sx.py)23
-rw-r--r--silx/test/utils.py35
-rw-r--r--silx/third_party/modest_image.py322
-rw-r--r--silx/third_party/setup.py1
-rw-r--r--silx/utils/ExternalResources.py321
-rw-r--r--silx/utils/array_like.py6
-rw-r--r--silx/utils/debug.py3
-rw-r--r--silx/utils/enum.py79
-rw-r--r--silx/utils/files.py (renamed from silx/third_party/enum.py)51
-rw-r--r--silx/utils/proxy.py41
-rw-r--r--silx/utils/test/__init__.py8
-rw-r--r--silx/utils/test/test_array_like.py10
-rw-r--r--silx/utils/test/test_enum.py96
-rw-r--r--silx/utils/test/test_external_resources.py99
-rw-r--r--silx/utils/test/test_number.py3
-rw-r--r--silx/utils/test/test_proxy.py53
-rw-r--r--silx/utils/test/test_weakref.py6
-rw-r--r--version.py4
352 files changed, 46985 insertions, 32961 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 073e1ac..ec0b01d 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,124 @@
Change Log
==========
+0.11.0: 2019/07/03
+------------------
+
+ * Graphical user interface:
+
+ * Plot:
+
+ * Add sample code on how to update a plot3d widget from a thread
+ * ScatterPlot: add the possibility to plot as a surface using Delaunay triangulation
+ * ScatterView: add a tool button to change scatter visualization mode (ex. Solid)
+ * (OpenGL backend) Fix memory leak when creating/deleting widgets in a loop
+
+
+ * Plot3D:
+
+ * Add an action to toggle plot3d's `PositionInfoWidget` picking.
+ * Add a 3D complex field visualization: Complex3DField (also available from silx view)
+ * Add a PositionInfoWidget and a tool button to toggle the picking mode to SceneWindow
+ * Add the possibility to render the scene with linear fog.
+
+ * `silx.gui.widgets`:
+
+ * Fix ImageFileDialog selection for a cube with shape like `1,y,x`.
+
+ * Miscellaneous:
+
+ * Requires numpy version >= 1.12
+ * HDF5 creator script
+ * Support of Python 3.4 is dropped. Please upgrade to at least Python 3.5.
+ * This is the last version to officially support Python 2.7.
+ * The source code is Python 3.8 ready.
+ * Improve PySide2 support. PyQt4 and PySide are deprecated.
+
+
+
+0.10.0: 2019/02/19
+------------------
+
+ * Graphical user interface:
+
+ * Plot:
+
+ * Add support of foreground color
+ * Fix plot background colors
+ * Add tool to mask ellipse
+ * StatsWidget:
+
+ * Add support for plot3D widgets
+ * Add a PyMca like widget
+
+ * `Colormap`: Phase colormap is now editable
+ * `ImageView`: Add ColorBarWidget
+ * `PrintPreview`:
+
+ * Add API to define 'comment' and 'title'
+ * Fix resizing in PyQt5
+
+ * Selection: Allow style definition
+ * `ColormapDialog`: display 'values' plot in log if colormap uses log
+ * Synchronize ColorBar with plot background colors
+ * `CurvesROIWidget`: ROI is now an object.
+
+ * Plot3D:
+
+ * `SceneWidget`: add ColormapMesh item
+ * Add compatibility with the StatsWidget to display statistic on 3D volumes.
+ * Add `ScalarFieldView.get|setOuterScale`
+ * Fix label update in param tree
+ * Add `ColormapMesh` item to the `SceneWidget`
+
+ * HDF5 tree:
+
+ * Allow URI drop
+ * Robustness of hdf5 tree with corrupted files
+
+ * `silx.gui.widgets`:
+
+ * Add URL selection table
+
+ * Input/output:
+
+ * Support compressed Fabio extensions
+ * Add a function to create external dataset for .vol file
+
+ * `silx view`:
+
+ * Support 2D view for 3D NXData
+ * Add a NXdata for complex images
+ * Add a 3d scalar field view to the NXdata views zoo
+ * Improve shortcuts, view loading
+ * Improve silx view loading, shortcuts and sliders ergonomy
+ * Support default attribute pointing to an NXdata at any group level
+
+ * `silx convert`
+
+ * Allow to use a filter id for compression
+
+ * Math:
+
+ * fft: multibackend fft
+
+ * OpenCL:
+
+ * Compute statistics on a numpy.ndarray
+ * Backprojection:
+
+ * Add sinogram filters (SinoFilter)
+ * Input and/or output can be device arrays.
+
+ * Miscellaneous:
+
+ * End of PySide support (use PyQt5)
+ * Last version supporting numpy 1.8.0. Next version will drop support for numpy < 1.12
+ * Python 2.7 support will be dropped before end 2019. From version 0.11, a deprecation warning will be issued.
+ * Remove some old deprecated methods/arguments
+ * Set Cython language_level to 3
+
+
0.9.0: 2018/10/23
-----------------
@@ -67,7 +185,7 @@ Change Log
* Graphical user interface:
* Plot:
-
+
* Adds support of x-axis date/time ticks for time series display (see `silx.gui.plot.items.XAxis.setTickMode`)
* Adds support of interactive authoring of regions of interest (see `silx.gui.plot.items.roi` and `silx.gui.plot.tools.roi`)
* Adds `StatsWidget` widget for displaying statistics on data displayed in a `PlotWidget`
@@ -201,7 +319,7 @@ Change Log
* OpenCl. Tomography. Implement a filtered back projection.
* Add a *PrintPreview* widget and a *PrintPreviewToolButton* for *PlotWidget*.
* Plot:
-
+
* Add a context menu on right click.
* Add a *ComplexImageView* widget.
* Merged abstract *Plot* class with *PlotWidget* class.
@@ -212,14 +330,14 @@ Change Log
* Refactor plot actions, new sub-package *silx.gui.plot.actions*.
* Add signals on *PlotWidget* items notifying updates.
* Mask. Support loading of TIFF images.
-
+
* Plot3d:
-
+
* Rework toolbar and interaction to use only the left mouse button.
* Support any colormap.
-
+
* Hdf5TreeView:
-
+
* Add an API to select a single tree node item (*setSelectedH5Node*)
* Better support and display of types.
* New column for displaying the kind of links.
@@ -229,25 +347,25 @@ Change Log
* Median filter. Add new modes (*reflect, mirror, shrink*) in addition to *nearest*.
* IO:
-
+
* Rename module *spectoh5* to *convert*. Add support for conversion of *fabio* formats.
* Support NPZ format.
* Support opening an URI (*silx.io.open(filename::path)*).
* *Group* methods *.keys*, *.value* and *.items* now return lists in Python 2
and iterators in Python 3.
-
+
* Image. Add tomography utils: *phantomgenerator* to produce Shepp-Logan phantom, function to compute center of rotation (*calc_center_corr*, *calc_center_centroid*) and rescale the intensity of an image (*rescale_intensity*).
-
+
* Commands:
-
+
* *silx view*:
-
+
* Add command line option *--use-opengl-plot*.
* Add command line option *--debug*, to print dataset reading errors.
* Support opening URI (*silx view filename::path*).
-
+
* *silx convert*. New command line application to convert supported data files to HDF5.
-
+
* Enable usage of *silx.resources* for other projects.
* The *silx* license is now fully MIT.
@@ -256,7 +374,7 @@ Change Log
-----------------
* Adds OpenGL backend to 1D and 2D graphics
- * Adds Object Oriented plot API with Curve, Histogram, Image, ImageRgba and Scatter items.
+ * Adds Object Oriented plot API with Curve, Histogram, Image, ImageRgba and Scatter items.
* Implements generic launcher (``silx view``)
* NXdataViewer. Module providing NeXus NXdata support
* Math/OpenCL. Implementation of median filter.
@@ -269,7 +387,7 @@ Change Log
* ROIs. Simplification of API: setRois, getRois, calculateRois.
* ROIs. Correction of calculation bug when the X-axis values were not ordered.
* Sift. Moves package from ``silx.image`` to ``silx.opencl``.
-
+
0.4.0: 2017/02/01
-----------------
@@ -283,7 +401,7 @@ Change Log
* Adds pixel intensity histogram action
* Adds histogram parameter to addCurve
* Refactoring. Create silx.gui.data (include widgets for data)
- * Refactoring. Rename utils.load as silx.io.open
+ * Refactoring. Rename utils.load as silx.io.open
* Changes active curve behavior in Plot. No default active curve is set by default
* Fit Action. Add polynomial functions and background customization
* PlotWindow. Provide API to access toolbar actions
@@ -305,7 +423,7 @@ Change Log
* Adds HDF5 load API (supporting Spec files) to silx.io.utils module
* Adds SpecFile support for multiple MCA headers
* Adds HDF5 TreeView
- * Adds FitManager to silx.math.fit and FitWidget to silx.gui.fit
+ * Adds FitManager to silx.math.fit and FitWidget to silx.gui.fit
* Adds ThreadPoolPushButton to silx.gui.widgets
* Adds getDataRange function to plot widget
* Adds loadUi, Slot and Property to qt.py
diff --git a/PKG-INFO b/PKG-INFO
index 52f365a..1f1f36b 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: silx
-Version: 0.9.0
+Version: 0.11.0
Summary: Software library for X-ray data analysis
Home-page: http://www.silx.org/
Author: data analysis unit
@@ -24,7 +24,8 @@ Description:
images file formats.
* OpenCL-based data processing: image alignment (SIFT),
image processing (median filter, histogram),
- filtered backprojection for tomography
+ filtered backprojection for tomography,
+ convolution
* Data reduction: histogramming, fitting, median filter
* A set of Qt widgets, including:
@@ -55,13 +56,13 @@ Description:
Or using Anaconda on Linux and MacOS:
- .. code-block:: bash
-
+ .. code-block:: bash
+
conda install silx -c conda-forge
Unofficial packages for different distributions are available:
- - Unofficial Debian8 packages are available at http://www.silx.org/pub/debian/
+ - Unofficial Debian9 packages are available at http://www.silx.org/pub/debian/
- CentOS 7 rpm packages are provided by Max IV at: http://pubrepo.maxiv.lu.se/rpm/el7/x86_64/
- Fedora 23 rpm packages are provided by Max IV at http://pubrepo.maxiv.lu.se/rpm/fc23/x86_64/
- Arch Linux (AUR) packages are also available: https://aur.archlinux.org/packages/python-silx
@@ -129,7 +130,6 @@ Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Cython
Classifier: Programming Language :: Python :: 2.7
-Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
diff --git a/README.rst b/README.rst
index bd91773..6b30551 100644
--- a/README.rst
+++ b/README.rst
@@ -16,7 +16,8 @@ The current version features:
images file formats.
* OpenCL-based data processing: image alignment (SIFT),
image processing (median filter, histogram),
- filtered backprojection for tomography
+ filtered backprojection for tomography,
+ convolution
* Data reduction: histogramming, fitting, median filter
* A set of Qt widgets, including:
@@ -47,13 +48,13 @@ To install silx with a minimal set of dependencies, run:
Or using Anaconda on Linux and MacOS:
-.. code-block:: bash
-
+.. code-block:: bash
+
conda install silx -c conda-forge
Unofficial packages for different distributions are available:
-- Unofficial Debian8 packages are available at http://www.silx.org/pub/debian/
+- Unofficial Debian9 packages are available at http://www.silx.org/pub/debian/
- CentOS 7 rpm packages are provided by Max IV at: http://pubrepo.maxiv.lu.se/rpm/el7/x86_64/
- Fedora 23 rpm packages are provided by Max IV at http://pubrepo.maxiv.lu.se/rpm/fc23/x86_64/
- Arch Linux (AUR) packages are also available: https://aur.archlinux.org/packages/python-silx
diff --git a/build-deb.sh b/build-deb.sh
index 208b706..f87e565 100755
--- a/build-deb.sh
+++ b/build-deb.sh
@@ -56,6 +56,9 @@ then
stretch)
debian_version=9
;;
+ buster)
+ debian_version=10
+ ;;
esac
fi
diff --git a/debian/changelog b/debian/changelog
index 2ac45dd..86bdf86 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,38 @@
+silx (0.11.0+dfsg-2) unstable; urgency=medium
+
+ * Use debhelper-compat instead of debian/compat.
+ * remove Python2 modules (Closes: #938481).
+ * Switched to compat level 12
+ - d/rules: Used --doc-main-package python3-silx.
+
+ -- Picca Frédéric-Emmanuel <picca@debian.org> Sat, 19 Oct 2019 13:19:44 +0200
+
+silx (0.11.0+dfsg-1) unstable; urgency=medium
+
+ [ Alexandre Marie ]
+ * Added test on openCL's use
+ * New upstream version 0.11.0+dfsg
+ * d/patches
+ - 0004-fix-missing-import.patches (Removed)
+ - 0005-fix-problem-with-sift-import (Removed)
+
+ -- Picca Frédéric-Emmanuel <picca@debian.org> Tue, 09 Jul 2019 15:26:55 +0200
+
+silx (0.10.1+dfsg-1~exp2) experimental; urgency=medium
+
+ * d/patchs
+ + 0005-fix-problem-with-sift-import.patch (Added)
+
+ -- Picca Frédéric-Emmanuel <picca@synchrotron-soleil.fr> Tue, 28 May 2019 11:17:46 +0200
+
+silx (0.10.1+dfsg-1~exp1) experimental; urgency=medium
+
+ * New upstream version 0.10.1+dfsg
+ * d/patches
+ + 0004-fix-missing-import.patch (Added)
+
+ -- Picca Frédéric-Emmanuel <picca@debian.org> Tue, 28 May 2019 08:20:44 +0200
+
silx (0.9.0+dfsg-3) unstable; urgency=medium
* d/rules: Do not run Qt test for now.
diff --git a/debian/compat b/debian/compat
deleted file mode 100644
index f599e28..0000000
--- a/debian/compat
+++ /dev/null
@@ -1 +0,0 @@
-10
diff --git a/debian/control b/debian/control
index 0d43d0b..e5a8e49 100644
--- a/debian/control
+++ b/debian/control
@@ -5,47 +5,14 @@ Uploaders: Jerome Kieffer <jerome.kieffer@esrf.fr>,
Alexandre Marie <alexandre.marie@synchrotron-soleil.fr>
Section: science
Priority: optional
-Build-Depends: cython (>= 0.23.2),
- cython-dbg (>= 0.23.2),
- cython3 (>= 0.23.2),
+Build-Depends: cython3 (>= 0.23.2),
cython3-dbg (>= 0.23.2),
- debhelper (>= 10),
+ debhelper-compat (= 12),
dh-python,
help2man,
- ipython,
- ipython-qtconsole,
ipython3,
ipython3-qtconsole,
- pandoc <!nodoc>,
- python-all-dbg,
- python-all-dev,
- python-concurrent.futures,
- python-fabio,
- python-fabio-dbg,
- python-h5py,
- python-h5py-dbg,
- python-mako,
- python-matplotlib,
- python-matplotlib-dbg,
- python-nbsphinx <!nodoc>,
- python-numpy,
- python-numpy-dbg,
- python-opengl,
- python-pil,
- python-pil-dbg,
- python-pyopencl,
- python-pyopencl-dbg,
- python-pyqt5,
- python-pyqt5-dbg,
- python-pyqt5.qtopengl,
- python-pyqt5.qtopengl-dbg,
- python-pyqt5.qtsvg,
- python-pyqt5.qtsvg-dbg,
- python-scipy,
- python-scipy-dbg,
- python-setuptools,
- python-sphinx,
- python-sphinxcontrib.programoutput,
+ pandoc <!nodoc>,
python3-all-dbg,
python3-all-dev,
python3-fabio,
@@ -108,72 +75,6 @@ Description: Toolbox for X-Ray data analysis - Executables
.
This uses the Python 3 version of the package.
-Package: python-silx
-Architecture: any
-Section: python
-Depends: ${misc:Depends}, ${python:Depends}, ${shlibs:Depends}
-Description: Toolbox for X-Ray data analysis - Python2 library
- The silx project aims at providing a collection of Python packages to
- support the development of data assessment, reduction and analysis
- applications at synchrotron radiation facilities. It aims at
- providing reading/writing different file formats, data reduction
- routines and a set of Qt widgets to browse and visualize data.
- .
- The current version provides :
- .
- * reading HDF5 file format (with support of SPEC file format)
- * histogramming
- * fitting
- * 1D and 2D visualization using multiple backends (matplotlib or OpenGL)
- * image plot widget with a set of associated tools (See changelog file).
- * Unified browser for HDF5, SPEC and image file formats supporting inspection
- and visualization of n-dimensional datasets.
- * Unified viewer (silx view filename) for HDF5, SPEC and image file formats
- * OpenGL-based widget to display 3D scalar field with
- isosurface and cutting plane.
- .
- This is the Python 2 version of the package.
-
-Package: python-silx-dbg
-Architecture: any
-Section: debug
-Depends: python-fabio-dbg,
- python-h5py-dbg,
- python-lxml-dbg,
- python-matplotlib-dbg,
- python-numpy-dbg,
- python-pil-dbg,
- python-pyopencl-dbg,
- python-pyqt5-dbg,
- python-pyqt5.qtopengl-dbg,
- python-pyqt5.qtsvg-dbg,
- python-scipy-dbg,
- python-silx (= ${binary:Version}),
- ${misc:Depends},
- ${python:Depends},
- ${shlibs:Depends}
-Description: Toolbox for X-Ray data analysis - Python2 debug
- The silx project aims at providing a collection of Python packages to
- support the development of data assessment, reduction and analysis
- applications at synchrotron radiation facilities. It aims at
- providing reading/writing different file formats, data reduction
- routines and a set of Qt widgets to browse and visualize data.
- .
- The current version provides :
- .
- * reading HDF5 file format (with support of SPEC file format)
- * histogramming
- * fitting
- * 1D and 2D visualization using multiple backends (matplotlib or OpenGL)
- * image plot widget with a set of associated tools (See changelog file).
- * Unified browser for HDF5, SPEC and image file formats supporting inspection
- and visualization of n-dimensional datasets.
- * Unified viewer (silx view filename) for HDF5, SPEC and image file formats
- * OpenGL-based widget to display 3D scalar field with
- isosurface and cutting plane.
- .
- This is the Python 2 debug version of the package.
-
Package: python3-silx
Architecture: any
Section: python
diff --git a/debian/gitlab-ci.yml b/debian/gitlab-ci.yml
index b7dc52a..33c3a64 100644
--- a/debian/gitlab-ci.yml
+++ b/debian/gitlab-ci.yml
@@ -1,16 +1,4 @@
-include: https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml
-
-build:
- extends: .build-unstable
-
-reprotest:
- extends: .test-reprotest
-
-lintian:
- extends: .test-lintian
-
-autopkgtest:
- extends: .test-autopkgtest
-
-piuparts:
- extends: .test-piuparts
+---
+include:
+ - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml
+ - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/pipeline-jobs.yml
diff --git a/debian/gitlab-ci.yml.tpl b/debian/gitlab-ci.yml.tpl
deleted file mode 100644
index eeb89b6..0000000
--- a/debian/gitlab-ci.yml.tpl
+++ /dev/null
@@ -1,3 +0,0 @@
-include: https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml
-
-# end of salsa pipeline bot parser
diff --git a/debian/patches/0002-use-the-system-mathjax-privacy-breach.patch b/debian/patches/0002-use-the-system-mathjax-privacy-breach.patch
index 641d90a..04deea7 100644
--- a/debian/patches/0002-use-the-system-mathjax-privacy-breach.patch
+++ b/debian/patches/0002-use-the-system-mathjax-privacy-breach.patch
@@ -8,10 +8,10 @@ Subject: use the system mathjax (privacy breach)
1 file changed, 5 insertions(+)
diff --git a/doc/source/conf.py b/doc/source/conf.py
-index 23efd15..532f6bf 100644
+index 86dbccf..18bfce2 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
-@@ -142,6 +142,11 @@ pygments_style = 'sphinx'
+@@ -143,6 +143,11 @@ pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
diff --git a/debian/patches/0003-do-not-modify-PYTHONPATH-from-setup.py.patch b/debian/patches/0003-do-not-modify-PYTHONPATH-from-setup.py.patch
index f4bbf44..4591cb6 100644
--- a/debian/patches/0003-do-not-modify-PYTHONPATH-from-setup.py.patch
+++ b/debian/patches/0003-do-not-modify-PYTHONPATH-from-setup.py.patch
@@ -7,10 +7,10 @@ Subject: do not modify PYTHONPATH from setup.py
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
-index 5ce0435..40b2e8e 100644
+index 1029bf0..46d0bdf 100644
--- a/setup.py
+++ b/setup.py
-@@ -257,7 +257,8 @@ class BuildMan(Command):
+@@ -260,7 +260,8 @@ class BuildMan(Command):
path.insert(0, os.path.abspath(build.build_lib))
env = dict((str(k), str(v)) for k, v in os.environ.items())
diff --git a/debian/patches/0003-fix-unit-test.patch b/debian/patches/0003-fix-unit-test.patch
index 07523e7..4e4f77f 100644
--- a/debian/patches/0003-fix-unit-test.patch
+++ b/debian/patches/0003-fix-unit-test.patch
@@ -8,7 +8,7 @@ Subject: fix unit test
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/silx/opencl/common.py b/silx/opencl/common.py
-index 9a04035..17c67d1 100644
+index 8d31c8a..1f9df50 100644
--- a/silx/opencl/common.py
+++ b/silx/opencl/common.py
@@ -61,7 +61,14 @@ else:
diff --git a/debian/patches/0004-fix-FTBFS-with-numpy-0.16.patch b/debian/patches/0004-fix-FTBFS-with-numpy-0.16.patch
deleted file mode 100644
index bcc32ad..0000000
--- a/debian/patches/0004-fix-FTBFS-with-numpy-0.16.patch
+++ /dev/null
@@ -1,24 +0,0 @@
-From: =?utf-8?q?Picca_Fr=C3=A9d=C3=A9ric-Emmanuel?=
- <picca@synchrotron-soleil.fr>
-Date: Thu, 21 Feb 2019 11:04:02 +0100
-Subject: fix FTBFS with numpy 0.16
-
----
- setup.py | 4 ++--
- 1 file changed, 2 insertions(+), 2 deletions(-)
-
-diff --git a/setup.py b/setup.py
-index 40b2e8e..9f6ae13 100644
---- a/setup.py
-+++ b/setup.py
-@@ -614,8 +614,8 @@ class BuildExt(build_ext):
- extern = 'extern "C" ' if ext.language == 'c++' else ''
- return_type = 'void' if sys.version_info[0] <= 2 else 'PyObject*'
-
-- ext.extra_compile_args.append(
-- '''-fvisibility=hidden -D'PyMODINIT_FUNC=%s__attribute__((visibility("default"))) %s ' ''' % (extern, return_type))
-+ # ext.extra_compile_args.append(
-+ # '''-fvisibility=hidden -D'PyMODINIT_FUNC=%s__attribute__((visibility("default"))) %s ' ''' % (extern, return_type))
-
- def is_debug_interpreter(self):
- """
diff --git a/debian/patches/0006-Tests-if-openCL-can-be-used.patch b/debian/patches/0006-Tests-if-openCL-can-be-used.patch
new file mode 100644
index 0000000..52a8c93
--- /dev/null
+++ b/debian/patches/0006-Tests-if-openCL-can-be-used.patch
@@ -0,0 +1,28 @@
+From: Alexandre Marie <alexandre.marie@synchrotron-soleil.fr>
+Date: Fri, 5 Jul 2019 16:52:20 +0200
+Subject: Tests if openCL can be used
+
+---
+ silx/opencl/common.py | 9 ++++++++-
+ 1 file changed, 8 insertions(+), 1 deletion(-)
+
+diff --git a/silx/opencl/common.py b/silx/opencl/common.py
+index 1f9df50..73cf676 100644
+--- a/silx/opencl/common.py
++++ b/silx/opencl/common.py
+@@ -60,7 +60,14 @@ else:
+ logger.warning("Unable to import pyOpenCl. Please install it from: http://pypi.python.org/pypi/pyopencl")
+ pyopencl = None
+ else:
+- import pyopencl.array as array
++ try:
++ pyopencl.get_platforms()
++ except pyopencl.LogicError:
++ logger.warning("The module pyOpenCL has been imported but can't be used here")
++ pyopencl = None
++ else:
++ import pyopencl.array as array
++ mf = pyopencl.mem_flags
+
+ if pyopencl is None:
+ class mf(object):
diff --git a/debian/patches/series b/debian/patches/series
index 57660df..1b65e43 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -1,4 +1,4 @@
0002-use-the-system-mathjax-privacy-breach.patch
0003-fix-unit-test.patch
0003-do-not-modify-PYTHONPATH-from-setup.py.patch
-0004-fix-FTBFS-with-numpy-0.16.patch
+0006-Tests-if-openCL-can-be-used.patch
diff --git a/debian/pydist-overrides b/debian/pydist-overrides
deleted file mode 100644
index 4a84372..0000000
--- a/debian/pydist-overrides
+++ /dev/null
@@ -1,3 +0,0 @@
-pyqt5 python-pyqt5,python-pyqt5.qtopengl,python-pyqt5.qtsvg
-enum34_python_version python-enum34
-futures_python_version python-concurrent.futures
diff --git a/debian/python-silx-doc.doc-base b/debian/python-silx-doc.doc-base
index b290d8a..c8efa7f 100644
--- a/debian/python-silx-doc.doc-base
+++ b/debian/python-silx-doc.doc-base
@@ -1,9 +1,9 @@
Document: silx-manual
Title: silx documentation manual
Author: Jérôme Kieffer <jerome.kieffer@esrf.eu>
-Abstract: Toolbox for X-Ray data analysis
+Abstract: Toolbox for X-Ray data analysis
Section: Science/Data Analysis
Format: HTML
-Index: /usr/share/doc/python-silx-doc/html/index.html
-Files: /usr/share/doc/python-silx-doc/html/*
+Index: /usr/share/doc/python3-silx/html/index.html
+Files: /usr/share/doc/python3-silx/html/*
diff --git a/debian/rules b/debian/rules
index 501910c..d086f63 100755
--- a/debian/rules
+++ b/debian/rules
@@ -9,6 +9,7 @@ export DEB_BUILD_MAINT_OPTIONS = hardening=+all
export PYBUILD_AFTER_INSTALL=rm -rf {destdir}/usr/bin/
export PYBUILD_NAME=silx
export SPECFILE_USE_GNU_SOURCE=1
+export SILX_FULL_INSTALL_REQUIRES=1
DEB_HOST_MULTIARCH ?= $(shell dpkg-architecture -qDEB_HOST_MULTIARCH)
@@ -23,7 +24,7 @@ ALL_PYX := $(call rwildcard,silx/,*.pyx)
PY3VER := $(shell py3versions -dv)
%:
- dh $@ --with python2,python3,sphinxdoc --buildsystem=pybuild
+ dh $@ --with python3,sphinxdoc --buildsystem=pybuild
override_dh_clean:
dh_clean
@@ -41,7 +42,6 @@ override_dh_auto_build:
dh_auto_build -- -s custom --build-args="env PYTHONPATH={build_dir} {interpreter} setup.py build_man"
override_dh_install:
- dh_numpy
dh_numpy3
# install scripts into silx
@@ -65,11 +65,9 @@ override_dh_python3:
# SILX_TEST_LOW_MEM=True to disable tests taking large amount of memory
# GPU=False to disable the use of a GPU with OpenCL test
# WITH_GL_TEST=False to disable tests using OpenGL
-# UNACTIVATED UNTIL dh_python from UNSTABLE IS FIXED
-# https://lists.debian.org/debian-python/2017/08/msg00095.html
override_dh_auto_test:
-# mkdir -p $(POCL_CACHE_DIR) # create POCL cachedir in order to avoid an FTBFS in sbuild
-# dh_auto_test -- -s custom --test-args="env PYTHONPATH={build_dir} GPU=False SILX_OPENCL=False SILX_TEST_LAW_MEM=True xvfb-run -a --server-args=\"-screen 0 1024x768x24\" {interpreter} run_tests.py -vv --installed"
+ mkdir -p $(POCL_CACHE_DIR) # create POCL cachedir in order to avoid an FTBFS in sbuild
+ dh_auto_test -- -s custom --test-args="env PYTHONPATH={build_dir} GPU=False WITH_QT_TEST=False SILX_OPENCL=False SILX_TEST_LAW_MEM=True xvfb-run -a --server-args=\"-screen 0 1024x768x24\" {interpreter} run_tests.py -vv --installed"
override_dh_installman:
dh_installman -p silx build/man/*.1
@@ -79,6 +77,6 @@ ifeq (,$(findstring nodocs, $(DEB_BUILD_OPTIONS)))
#mkdir -p $(POCL_CACHE_DIR) # create POCL cachedir in order to avoid an FTBFS in sbuild
mkdir -p -m 700 $(XDG_RUNTIME_DIR)
pybuild --build -s custom -p $(PY3VER) --build-args="cd doc && env PYTHONPATH={build_dir} http_proxy='127.0.0.1:9' xvfb-run -a --server-args=\"-screen 0 1024x768x24\" {interpreter} -m sphinx -N -bhtml source build/html"
- dh_installdocs "doc/build/html" -p python-silx-doc
+ dh_installdocs "doc/build/html" -p python-silx-doc --doc-main-package=python3-silx
dh_sphinxdoc -O--buildsystem=pybuild
endif
diff --git a/debian/tests/control b/debian/tests/control
new file mode 100644
index 0000000..deb174c
--- /dev/null
+++ b/debian/tests/control
@@ -0,0 +1,15 @@
+Test-Command: set -efu
+ ; for py in $(py3versions -r 2>/dev/null)
+ ; do cd "$AUTOPKGTEST_TMP"
+ ; echo "Testing with $py:"
+ ; xvfb-run -a --server-args="-screen 0 1024x768x24" $py -c "import silx.test; silx.test.run_tests()" 2>&1
+ ; done
+Depends: python3-all, python3-silx, xauth, xvfb
+
+Test-Command: set -efu
+ ; for py in $(py3versions -r 2>/dev/null)
+ ; do cd "$AUTOPKGTEST_TMP"
+ ; echo "Testing with $py-dbg:"
+ ; xvfb-run -a --server-args="-screen 0 1024x768x24" $py-dbg -c "import silx.test; silx.test.run_tests()" 2>&1
+ ; done
+Depends: python3-all-dbg, python3-silx-dbg, xauth, xvfb
diff --git a/debian/tests/control.autodep8 b/debian/tests/control.autodep8
deleted file mode 100644
index 5ffa42b..0000000
--- a/debian/tests/control.autodep8
+++ /dev/null
@@ -1,31 +0,0 @@
-Test-Command: set -efu
- ; for py in $(pyversions -r 2>/dev/null)
- ; do cd "$AUTOPKGTEST_TMP"
- ; echo "Testing with $py:"
- ; xvfb-run -a --server-args="-screen 0 1024x768x24" $py -m unittest discover silx 2>&1
- ; done
-Depends: python-all, python-silx, xauth, xvfb
-
-Test-Command: set -efu
- ; for py in $(pyversions -r 2>/dev/null)
- ; do cd "$AUTOPKGTEST_TMP"
- ; echo "Testing with $py-dbg:"
- ; xvfb-run -a --server-args="-screen 0 1024x768x24" $py-dbg -m unittest discover silx 2>&1
- ; done
-Depends: python-all-dbg, python-silx-dbg, xauth, xvfb
-
-Test-Command: set -efu
- ; for py in $(py3versions -r 2>/dev/null)
- ; do cd "$AUTOPKGTEST_TMP"
- ; echo "Testing with $py:"
- ; xvfb-run -a --server-args="-screen 0 1024x768x24" $py -m unittest discover silx 2>&1
- ; done
-Depends: python3-all, python3-silx, xauth, xvfb
-
-Test-Command: set -efu
- ; for py in $(py3versions -r 2>/dev/null)
- ; do cd "$AUTOPKGTEST_TMP"
- ; echo "Testing with $py-dbg:"
- ; xvfb-run -a --server-args="-screen 0 1024x768x24" $py-dbg -m unittest discover silx 2>&1
- ; done
-Depends: python3-all-dbg, python3-silx-dbg, xauth, xvfb
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 532f6bf..18bfce2 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# /*##########################################################################
-# Copyright (C) 2015-2017 European Synchrotron Radiation Facility
+# Copyright (C) 2015-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -76,6 +76,7 @@ extensions = [
'sphinx.ext.viewcode',
'sphinx.ext.doctest',
'sphinxext-archive',
+ 'snapshotqt_directive',
'nbsphinx'
]
diff --git a/doc/source/ext/snapshotqt_directive.py b/doc/source/ext/snapshotqt_directive.py
new file mode 100644
index 0000000..81305e0
--- /dev/null
+++ b/doc/source/ext/snapshotqt_directive.py
@@ -0,0 +1,242 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2004-2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# 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.
+#
+# ###########################################################################*/
+"""RST directive to include snapshot of a Qt application in Sphinx doc.
+
+Configuration variable in conf.py:
+
+- snapshotqt_image_type: image file extension (default 'png').
+- snapshotqt_script_dir: relative path of the root directory for scripts from
+ the documentation source directory (i.e., the directory of conf.py)
+ (default: '..').
+"""
+from __future__ import absolute_import
+
+__authors__ = ["H. Payno", "T. Vincent"]
+__license__ = "MIT"
+__date__ = "07/12/2018"
+
+import os
+import logging
+import sys
+from docutils.parsers.rst.directives.images import Image
+from docutils.parsers.rst import directives
+
+# from docutils.par
+# note: conf.py is patching the PATH so this will be the 'current' qt version
+
+home = os.path.abspath(os.path.join(__file__, "..", "..", "..", '..'))
+
+
+if not os.environ.get('DIRECTIVE_SNAPSHOT_QT') == 'True':
+ """
+ In case we don't wan't to regenerate screenshot, simply apply Figure
+ directive
+ """
+ class SnapshotQtDirective(Image):
+ option_spec = Image.option_spec.copy()
+ option_spec['script'] = directives.unchanged
+ has_content = True
+
+ def run(self):
+ self.options['figwidth'] = 'image'
+ self.content = []
+
+ # Create an image filename from arguments
+ return Image.run(self)
+
+ def makescreenshot(*args, **kwargs):
+ raise RuntimeError('not defined without env variable SILX_GENERATE_SCREENSHOT set to True')
+
+ def setup(app):
+ app.add_config_value('snapshotqt_image_type', 'png', 'env')
+ app.add_config_value('snapshotqt_script_dir', '..', 'env')
+ app.add_directive('snapshotqt', SnapshotQtDirective)
+ return {'version': '0.1'}
+
+else:
+ from silx.gui import qt
+
+ logging.basicConfig()
+ _logger = logging.getLogger(__name__)
+
+ # RST directive ###############################################################
+
+ class SnapshotQtDirective(Image):
+ """Image of a Qt application snapshot.
+
+ Directive Type: "snapshotqt"
+ Doctree Elements: As for figure
+ Directive Arguments: One or more, required (script URI + script arguments).
+ Directive Options: Possible.
+ Directive Content: Interpreted as the figure caption and optional legend.
+
+ A "snapshotqt" is a rst `figure
+ <http://docutils.sourceforge.net/docs/ref/rst/directives.html#figure>`_
+ that is generated from a Python script that uses Qt.
+
+ The path of the script to take a snapshot is relative to
+ the path given in conf.py 'snapshotqt_script_dir' value.
+
+ ::
+
+ .. snapshotqt: img/demo.py
+ :align: center
+ :height: 5cm
+
+ source code
+
+
+ you can also define a snapshot from a script, using the :script: option
+ .. note:: on this path are given from the project root level
+
+ ::
+ .. snapshotqt: img/demo.py
+ :align: center
+ :height: 5cm
+ :script: myscript.py
+ """
+ option_spec = Image.option_spec.copy()
+ option_spec['script'] = directives.unchanged
+ has_content = True
+
+ def run(self):
+ assert len(self.arguments) > 0
+ # Run script stored in arguments and replace by snapshot filename
+ script = self.options.pop('script', None)
+ env = self.state.document.settings.env
+
+ image_ext = env.config.snapshotqt_image_type.lower()
+ script_name = self.arguments[0].replace(image_ext, 'py')
+ output_script = os.path.join(env.app.outdir, script_name)
+
+ image_file_source_path = env.relfn2path(self.arguments[0])[0]
+ image_file_source_path = os.path.join(home, env.srcdir, image_file_source_path)
+
+ def createNeededDirs(_dir):
+ parentDir = os.path.dirname(_dir)
+ if parentDir not in ('', os.sep):
+ createNeededDirs(parentDir)
+ if os.path.exists(_dir) is False:
+ os.mkdir(_dir)
+
+ createNeededDirs(os.path.dirname(output_script))
+
+ has_source_code = not (self.content is None or len(self.content) is 0)
+ if has_source_code:
+ with open(output_script, 'w') as _file:
+ _file.write("# from silx.gui import qt\n")
+ _file.write("# app = qt.QApplication([])\n")
+ for _line in self.content:
+ _towrite = _line.lstrip(' ')
+ if not _towrite.startswith(':'):
+ _file.write(_towrite + '\n')
+ _file.write("app.exec_()")
+ self.content = []
+ if script is not None:
+ _logger.warning('Cannot specify a script if source code (content) is given.'
+ 'Ignore script option')
+ makescreenshot(script_or_module=output_script,
+ filename=image_file_source_path)
+ else:
+ # script
+ if script is None:
+ _logger.warning('no source code or script defined in the snapshot'
+ 'directive, fail to generate a screenshot')
+ else:
+ script_path = os.path.join(home, script)
+ makescreenshot(script_or_module=script_path,
+ filename=image_file_source_path)
+
+ #
+ # Use created image as in Figure
+ return super(SnapshotQtDirective, self).run()
+
+ def setup(app):
+ app.add_config_value('snapshotqt_image_type', 'png', 'env')
+ app.add_config_value('snapshotqt_script_dir', '..', 'env')
+ app.add_directive('snapshotqt', SnapshotQtDirective)
+ return {'version': '0.1'}
+
+ # screensImageFileDialogH5.hot function ########################################################
+
+ def makescreenshot(script_or_module, filename):
+ _logger.info('generate screenshot for %s from %s, binding is %s'
+ '' % (filename, script_or_module, qt.BINDING))
+
+ # Probe Qt binding
+ if qt.BINDING == 'PyQt4':
+ def grabWindow(winID):
+ return qt.QPixmap.grabWindow(winID)
+ elif qt.BINDING in ('PyQt5', 'PySide2'):
+ def grabWindow(winID):
+ screen = qt.QApplication.primaryScreen()
+ return screen.grabWindow(winID)
+
+ global _count
+ _count = 15
+ global _TIMEOUT
+ _TIMEOUT = 1000. # in ms
+ app = qt.QApplication.instance() or qt.QApplication([])
+ _logger.debug('Using Qt bindings: %s', qt)
+
+ def _grabActiveWindowAndClose():
+ global _count
+ activeWindow = qt.QApplication.activeWindow()
+ if activeWindow is not None:
+ if activeWindow.isVisible():
+ # hot fix since issue with pySide2 API
+ if qt.BINDING == 'PySide2':
+ pixmap = activeWindow.grab()
+ else:
+ pixmap = grabWindow(activeWindow.winId())
+ saveOK = pixmap.save(filename)
+ if not saveOK:
+ _logger.error(
+ 'Cannot save snapshot to %s', filename)
+ else:
+ _logger.error('activeWindow is not visible.')
+ app.quit()
+ else:
+ _count -= 1
+ if _count > 0:
+ # Only restart a timer if everything is OK
+ qt.QTimer.singleShot(_TIMEOUT,
+ _grabActiveWindowAndClose)
+ else:
+ app.quit()
+ raise TimeoutError(
+ 'Aborted: It took too long to have an active window.')
+ script_or_module = os.path.abspath(script_or_module)
+
+ sys.argv = [script_or_module]
+ sys.path.append(
+ os.path.abspath(os.path.dirname(script_or_module)))
+ qt.QTimer.singleShot(_TIMEOUT, _grabActiveWindowAndClose)
+ if sys.version_info < (3, ):
+ execfile(script_or_module)
+ else:
+ with open(script_or_module) as f:
+ code = compile(f.read(), script_or_module, 'exec')
+ exec(code, globals(), locals())
diff --git a/doc/source/install.rst b/doc/source/install.rst
index 7941a7b..3862e4c 100644
--- a/doc/source/install.rst
+++ b/doc/source/install.rst
@@ -110,7 +110,7 @@ Linux
Packages are available for a few distributions:
-- Debian 8: see `Installing a Debian package`_.
+- Debian 9: see `Installing a Debian package`_.
- `CentOS 7 RPM packages <http://pubrepo.maxiv.lu.se/rpm/el7/x86_64/>`_ provided by the Max IV institute at Lund, Sweden.
- `Fedora 23 rpm packages <http://pubrepo.maxiv.lu.se/rpm/fc23/x86_64/>`_ provided by the Max IV institute at Lund, Sweden.
- `Arch Linux (AUR) package <https://aur.archlinux.org/packages/python-silx>`_ provided by Leonid Bloch.
@@ -125,7 +125,7 @@ You can also follow one of those installation procedures:
Installing a Debian package
+++++++++++++++++++++++++++
-Debian 8 (Jessie) packages are available on http://www.silx.org/pub/debian/ for amd64 computers.
+Debian 9 (Stretch) packages are available on http://www.silx.org/pub/debian/ for amd64 computers.
To install it, you need to download this file :
.. code-block:: bash
diff --git a/doc/source/modules/gui/data/img/ArrayTableWidget.png b/doc/source/modules/gui/data/img/ArrayTableWidget.png
index 7c81d02..6ae9114 100644
--- a/doc/source/modules/gui/data/img/ArrayTableWidget.png
+++ b/doc/source/modules/gui/data/img/ArrayTableWidget.png
Binary files differ
diff --git a/doc/source/modules/gui/data/img/DataViewer.png b/doc/source/modules/gui/data/img/DataViewer.png
index 719c822..c2185d3 100644
--- a/doc/source/modules/gui/data/img/DataViewer.png
+++ b/doc/source/modules/gui/data/img/DataViewer.png
Binary files differ
diff --git a/doc/source/modules/gui/gallery.rst b/doc/source/modules/gui/gallery.rst
index fffcfd8..9923c0b 100644
--- a/doc/source/modules/gui/gallery.rst
+++ b/doc/source/modules/gui/gallery.rst
@@ -39,14 +39,27 @@ Widgets gallery
* - Widget
- Description
- * - .. image:: data/img/ArrayTableWidget.png
+ * - .. snapshotqt:: data/img/ArrayTableWidget.png
:height: 150px
:align: center
+
+ from silx.gui.data.ArrayTableWidget import ArrayTableWidget
+ import numpy.random
+ table = ArrayTableWidget()
+ table.setArrayData(numpy.random.random((100, 100, 100)))
+ table.resize(500, 300)
+ table.show()
- :class:`ArrayTableWidget` is a table widget with browsers designed to
display the content of multi-dimensional data arrays.
- * - .. image:: data/img/DataViewer.png
+ * - .. snapshotqt:: data/img/DataViewer.png
:height: 150px
:align: center
+
+ import numpy.random
+ from silx.gui.data.DataViewer import DataViewer
+ viewer = DataViewer()
+ viewer.setData(numpy.random.random((100, 100, 100)))
+ viewer.show()
- :class:`DataViewer` is a widget designed to display data using the most
adapted view.
* - .. image:: data/img/DataViewerFrame.png
@@ -208,14 +221,33 @@ Additional widgets:
:align: center
- :class:`.PlotTools.PositionInfo` is a widget displaying mouse position and
information of a :class:`PlotWidget` associated to the mouse position.
- * - .. image:: plot/img/LimitsToolBar.png
+ * - .. snapshotqt:: plot/img/LimitsToolBar.png
:width: 300px
:align: center
+
+ from silx.gui.plot import Plot2D
+ from silx.gui.plot.tools.LimitsToolBar import LimitsToolBar
+ plot = Plot2D()
+ toolbar = LimitsToolBar(plot=plot)
+ toolbar.resize(400, 30)
+ plot.show()
+ toolbar.show()
+ app.processEvents()
- :class:`.PlotTools.LimitsToolBar` is a QToolBar displaying and
controlling the limits of a :class:`PlotWidget`.
- * - .. image:: plot/img/logColorbar.png
+ * - .. snapshotqt:: plot/img/logColorbar.png
:height: 150px
:align: center
+
+ from silx.gui.plot import Plot2D
+ from silx.gui.plot.ColorBar import ColorBarWidget
+ from silx.gui.plot.Colors import Colormap
+ import numpy
+ plot = Plot2D()
+ colorbar = ColorBarWidget(plot=plot, legend='Colormap Log scale')
+ colorbar.setColormap(Colormap(name='jet', normalization='log', vmin=1.0, vmax=10e3))
+ colorbar.show()
+ colorbar.resize(20, 500)
- :class:`.ColorBar.ColorBarWidget` display colormap gradient and can be linked with a plot
to display the colormap
* - .. image:: plot/img/statsWidget.png
@@ -243,9 +275,10 @@ Additional widgets:
and associated toolbars.
It can display 2D images, 2D scatter data, 3D scatter data and 3D volumes with different visualizations.
See ``plot3dSceneWindow.py`` in :ref:`plot3d-sample-code`.
- * - .. image:: plot3d/img/SceneWidget.png
+ * - .. snapshotqt:: plot3d/img/SceneWidget.png
:height: 150px
:align: center
+ :script: examples/plot3dSceneWindow.py
- :class:`SceneWidget` is a :class:`Plot3DWidget` providing a 3D scene for visualizing different kind of data.
It can display 2D images, 2D scatter data, 3D scatter data and 3D volumes with different visualizations.
See ``plot3dSceneWindow.py`` in :ref:`plot3d-sample-code`.
@@ -303,49 +336,103 @@ Additional widgets:
* - Widget
- Description
- * - .. image:: widgets/img/FrameBrowser.png
+ * - .. snapshotqt:: widgets/img/FrameBrowser.png
:width: 110px
:align: center
+
+ from silx.gui.widgets.FrameBrowser import FrameBrowser
+ widget = FrameBrowser()
+ widget.setRange(0, 10)
+ widget.show()
- :class:`FrameBrowser.FrameBrowser` is a browser widget designed to
browse through a sequence of integers (e.g. the indices of an array)
- * - .. image:: widgets/img/HorizontalSliderWithBrowser.png
+ * - .. snapshotqt:: widgets/img/HorizontalSliderWithBrowser.png
:width: 150px
:align: center
+
+ from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser
+ slider = HorizontalSliderWithBrowser()
+ slider.show()
- :class:`FrameBrowser.HorizontalSliderWithBrowser` is a :class:`FrameBrowser`
with an additional slider.
- * - .. image:: widgets/img/RangeSlider.png
+ * - .. snapshotqt:: widgets/img/RangeSlider.png
:width: 150px
:align: center
+
+ from silx.gui.widgets.RangeSlider import RangeSlider
+ from silx.gui.plot.Colors import Colormap
+ import numpy
+ widget = RangeSlider()
+ widget.setRange(0, 500)
+ widget.setValues(100, 400)
+ background = numpy.sin(numpy.arange(250) / 250.0)
+ background[0], background[-1] = background[-1], background[0]
+ colormap = Colormap("viridis")
+ widget.setGroovePixmapFromProfile(background, colormap)
+ widget.show()
- :class:`~silx.gui.widgets.RangeSlider.RangeSlider` is a slider with 2 thumbs dedicated
to the interactive selection of an interval.
- * - .. image:: widgets/img/PeriodicCombo.png
+ * - .. snapshotqt:: widgets/img/PeriodicCombo.png
:width: 150px
:align: center
+
+ from silx.gui.widgets.PeriodicTable import PeriodicCombo
+ widget = PeriodicCombo()
+ widget.setSelection('Yb')
+ widget.show()
- :class:`PeriodicTable.PeriodicCombo` is a :class:`QComboBox` widget designed to
select a single atomic element.
- * - .. image:: widgets/img/PeriodicList.png
+ * - .. snapshotqt:: widgets/img/PeriodicList.png
:height: 150px
:align: center
+
+ from silx.gui.widgets.PeriodicTable import PeriodicList
+ widget = PeriodicList()
+ widget.setSelectedElements(('S', 'Cl'))
+ widget.resize(200, 400)
+ widget.show()
- :class:`PeriodicTable.PeriodicList` is a :class:`QTreeWidget` designed to select one
or more atomic elements.
- * - .. image:: widgets/img/PeriodicTable.png
+ * - .. snapshotqt:: widgets/img/PeriodicTable.png
:height: 150px
:align: center
+
+ from silx.gui.widgets.PeriodicTable import PeriodicTable
+ widget = PeriodicTable()
+ widget.setSelection(('S', 'H', 'Zr'))
+ widget.show()
- :class:`PeriodicTable.PeriodicTable` is a periodic table widget designed to select one
or more atomic elements.
- * - .. image:: widgets/img/TableWidget.png
+ * - .. snapshotqt:: widgets/img/TableWidget.png
:height: 150px
:align: center
+
+ from silx.gui.widgets.TableWidget import TableWidget
+ widget = TableWidget()
+ widget.setRowCount(8)
+ widget.setColumnCount(4)
+ widget.resize(300, 200)
+ widget.show()
- :class:`TableWidget.TableWidget` and :class:`TableWidget.TableView` inherit respectively
:class:`QTableWidget` and :class:`QTableView`, and add a context menu with *cut/copy/paste*
actions.
- * - .. image:: widgets/img/WaitingPushButton.png
+ * - .. snapshotqt:: widgets/img/WaitingPushButton.png
:width: 60px
:align: center
+
+ from silx.gui.widgets.WaitingPushButton import WaitingPushButton
+ from silx.gui import icons
+ animated_icon = icons.getWaitIcon()
+ button = WaitingPushButton(icon=animated_icon.currentIcon(), text='Run')
+ button.show()
- :class:`WaitingPushButton` is a :class:`QPushButton` that can be graphically disabled,
for example to wait for a callback function to finish computing.
- * - .. image:: widgets/img/ThreadPoolPushButton.png
+ * - .. snapshotqt:: widgets/img/ThreadPoolPushButton.png
:width: 100px
:align: center
+
+ from silx.gui.widgets.ThreadPoolPushButton import ThreadPoolPushButton
+ button = ThreadPoolPushButton(text="Compute 2^16")
+ button.show()
- :class:`ThreadPoolPushButton` is a :class:`WaitingPushButton` that executes a
callback in a thread.
diff --git a/doc/source/modules/gui/icons.rst b/doc/source/modules/gui/icons.rst
index 659bc4c..dbd58b4 100644
--- a/doc/source/modules/gui/icons.rst
+++ b/doc/source/modules/gui/icons.rst
@@ -75,6 +75,8 @@ Available icons
- compare-align-stretch
* - |compare-keypoints|
- compare-keypoints
+ * - |compare-mode-a-minus-b|
+ - compare-mode-a-minus-b
* - |compare-mode-a|
- compare-mode-a
* - |compare-mode-b|
@@ -121,6 +123,8 @@ Available icons
- draw-rubber
* - |edit-copy|
- edit-copy
+ * - |eye|
+ - eye
* - |first|
- first
* - |folder|
@@ -251,6 +255,8 @@ Available icons
- plot-ylog
* - |plot-yup|
- plot-yup
+ * - |pointing-hand|
+ - pointing-hand
* - |previous|
- previous
* - |profile-clear|
@@ -370,6 +376,7 @@ Available icons
.. |compare-align-origin| image:: ../../../../silx/resources/gui/icons/compare-align-origin.png
.. |compare-align-stretch| image:: ../../../../silx/resources/gui/icons/compare-align-stretch.png
.. |compare-keypoints| image:: ../../../../silx/resources/gui/icons/compare-keypoints.png
+.. |compare-mode-a-minus-b| image:: ../../../../silx/resources/gui/icons/compare-mode-a-minus-b.png
.. |compare-mode-a| image:: ../../../../silx/resources/gui/icons/compare-mode-a.png
.. |compare-mode-b| image:: ../../../../silx/resources/gui/icons/compare-mode-b.png
.. |compare-mode-hline| image:: ../../../../silx/resources/gui/icons/compare-mode-hline.png
@@ -393,6 +400,7 @@ Available icons
.. |draw-pencil| image:: ../../../../silx/resources/gui/icons/draw-pencil.png
.. |draw-rubber| image:: ../../../../silx/resources/gui/icons/draw-rubber.png
.. |edit-copy| image:: ../../../../silx/resources/gui/icons/edit-copy.png
+.. |eye| image:: ../../../../silx/resources/gui/icons/eye.png
.. |first| image:: ../../../../silx/resources/gui/icons/first.png
.. |folder| image:: ../../../../silx/resources/gui/icons/folder.png
.. |image-mask| image:: ../../../../silx/resources/gui/icons/image-mask.png
@@ -458,6 +466,7 @@ Available icons
.. |plot-ydown| image:: ../../../../silx/resources/gui/icons/plot-ydown.png
.. |plot-ylog| image:: ../../../../silx/resources/gui/icons/plot-ylog.png
.. |plot-yup| image:: ../../../../silx/resources/gui/icons/plot-yup.png
+.. |pointing-hand| image:: ../../../../silx/resources/gui/icons/pointing-hand.png
.. |previous| image:: ../../../../silx/resources/gui/icons/previous.png
.. |profile-clear| image:: ../../../../silx/resources/gui/icons/profile-clear.png
.. |profile1D| image:: ../../../../silx/resources/gui/icons/profile1D.png
diff --git a/doc/source/modules/gui/plot/dev.rst b/doc/source/modules/gui/plot/dev.rst
index 8966487..0c848e9 100644
--- a/doc/source/modules/gui/plot/dev.rst
+++ b/doc/source/modules/gui/plot/dev.rst
@@ -92,6 +92,7 @@ The following modules are the modules used internally by the plot package.
.. automodule:: silx.gui.plot.CurvesROIWidget
:members:
+ :noindex:
:mod:`Interaction`
++++++++++++++++++
diff --git a/doc/source/modules/gui/plot/img/BasicGridStatsWidget.png b/doc/source/modules/gui/plot/img/BasicGridStatsWidget.png
new file mode 100644
index 0000000..53ddc0e
--- /dev/null
+++ b/doc/source/modules/gui/plot/img/BasicGridStatsWidget.png
Binary files differ
diff --git a/doc/source/modules/gui/plot/img/BasicStatsWidget.png b/doc/source/modules/gui/plot/img/BasicStatsWidget.png
new file mode 100644
index 0000000..c9ed2cd
--- /dev/null
+++ b/doc/source/modules/gui/plot/img/BasicStatsWidget.png
Binary files differ
diff --git a/doc/source/modules/gui/plot/img/LimitsToolBar.png b/doc/source/modules/gui/plot/img/LimitsToolBar.png
index 99af8bd..54b6c2b 100644
--- a/doc/source/modules/gui/plot/img/LimitsToolBar.png
+++ b/doc/source/modules/gui/plot/img/LimitsToolBar.png
Binary files differ
diff --git a/doc/source/modules/gui/plot/img/logColorbar.png b/doc/source/modules/gui/plot/img/logColorbar.png
index cdd247c..c677a9b 100644
--- a/doc/source/modules/gui/plot/img/logColorbar.png
+++ b/doc/source/modules/gui/plot/img/logColorbar.png
Binary files differ
diff --git a/doc/source/modules/gui/plot/index.rst b/doc/source/modules/gui/plot/index.rst
index 6f06830..01cb29b 100644
--- a/doc/source/modules/gui/plot/index.rst
+++ b/doc/source/modules/gui/plot/index.rst
@@ -61,6 +61,13 @@ Additionnal plot tool widgets:
statswidget.rst
stats/index.rst
+Utilities
+
+.. toctree::
+ :maxdepth: 2
+
+ utils.rst
+
Internals
---------
diff --git a/doc/source/modules/gui/plot/roi.rst b/doc/source/modules/gui/plot/roi.rst
index 77b5c2a..efe41a7 100644
--- a/doc/source/modules/gui/plot/roi.rst
+++ b/doc/source/modules/gui/plot/roi.rst
@@ -1,12 +1,15 @@
-.. currentmodule:: silx.gui.plot
+.. currentmodule:: silx.gui.plot.CurvesROIWidget
:mod:`CurvesROIWidget`: ROI from curves
=======================================
+
.. |roiWidgetImage| image:: img/roiwidget.png
:height: 400px
:align: middle
+.. automodule:: silx.gui.plot.CurvesROIWidget
+
You can access to the ROIWidget from a Plot window by :
- using the tool button 'ROI'
@@ -14,4 +17,28 @@ You can access to the ROIWidget from a Plot window by :
|roiWidgetImage|
-.. automodule:: silx.gui.plot.CurvesROIWidget
+
+
+
+:class:`ROI` class
+------------------
+
+.. autoclass:: ROI
+ :show-inheritance:
+ :members:
+
+
+:class:`CurvesROIWidget` class
+-------------------------------
+
+.. autoclass:: CurvesROIWidget
+ :show-inheritance:
+ :members:
+
+
+:class:`ROITable` class
+-----------------------
+
+.. autoclass:: ROITable
+ :show-inheritance:
+ :members:
diff --git a/doc/source/modules/gui/plot/statswidget.rst b/doc/source/modules/gui/plot/statswidget.rst
index f534921..1574abc 100644
--- a/doc/source/modules/gui/plot/statswidget.rst
+++ b/doc/source/modules/gui/plot/statswidget.rst
@@ -31,3 +31,19 @@
:show-inheritance:
:members:
+
+:class:`BasicLineStatsWidget` class
+-----------------------------------
+
+.. autoclass:: BasicLineStatsWidget
+ :show-inheritance:
+ :members:
+
+
+:class:`BasicGridStatsWidget` class
+-----------------------------------
+
+.. autoclass:: BasicGridStatsWidget
+ :show-inheritance:
+ :members:
+
diff --git a/doc/source/modules/gui/plot/utils.rst b/doc/source/modules/gui/plot/utils.rst
new file mode 100644
index 0000000..e930208
--- /dev/null
+++ b/doc/source/modules/gui/plot/utils.rst
@@ -0,0 +1,12 @@
+.. currentmodule:: silx.gui.plot.utils
+
+
+:mod:`axis`: utilities for plots
+================================
+
+SyncAxes
+--------
+
+.. autoclass:: silx.gui.plot.utils.axis.SyncAxes
+ :members:
+
diff --git a/doc/source/modules/gui/plot3d/glutils.rst b/doc/source/modules/gui/plot3d/glutils.rst
index 2c36e83..21781d9 100644
--- a/doc/source/modules/gui/plot3d/glutils.rst
+++ b/doc/source/modules/gui/plot3d/glutils.rst
@@ -13,13 +13,17 @@
Utility functions
-----------------
-.. currentmodule:: silx.gui._glutils
+.. currentmodule:: silx.gui._glutils.Context
For OpenGL context management:
-.. autofunction:: getGLContext
+.. autofunction:: getCurrent
+
+.. autofunction:: setCurrent
-.. autofunction:: setGLContextGetter
+.. autofunction:: current
+
+.. currentmodule:: silx.gui._glutils
For type checking and conversion:
diff --git a/doc/source/modules/gui/plot3d/img/SceneWidget.png b/doc/source/modules/gui/plot3d/img/SceneWidget.png
index 610c41a..dbe7791 100644
--- a/doc/source/modules/gui/plot3d/img/SceneWidget.png
+++ b/doc/source/modules/gui/plot3d/img/SceneWidget.png
Binary files differ
diff --git a/doc/source/modules/gui/plot3d/items.rst b/doc/source/modules/gui/plot3d/items.rst
index 1162cb9..5c4884f 100644
--- a/doc/source/modules/gui/plot3d/items.rst
+++ b/doc/source/modules/gui/plot3d/items.rst
@@ -53,7 +53,7 @@ The following classes are items that describes the content of a :class:`SceneWid
:class:`Scatter2D` inherits from :class:`.DataItem3D` and also provides its API.
.. autoclass:: Scatter2D
- :members: getData, setData, getXData, getYData, getValues,
+ :members: getData, setData, getXData, getYData, getValueData,
supportedVisualizations, isPropertyEnabled,
getVisualization, setVisualization,
isHeightMap, setHeightMap,
@@ -67,7 +67,7 @@ The following classes are items that describes the content of a :class:`SceneWid
:class:`Scatter3D` inherits from :class:`.DataItem3D` and also provides its API.
.. autoclass:: Scatter3D
- :members: getData, setData, getXData, getYData, getZData, getValues,
+ :members: getData, setData, getXData, getYData, getZData, getValueData,
getColormap, setColormap,
getSupportedSymbols, getSymbol, setSymbol
diff --git a/doc/source/modules/gui/widgets/img/FrameBrowser.png b/doc/source/modules/gui/widgets/img/FrameBrowser.png
index c5624f7..3843e70 100644
--- a/doc/source/modules/gui/widgets/img/FrameBrowser.png
+++ b/doc/source/modules/gui/widgets/img/FrameBrowser.png
Binary files differ
diff --git a/doc/source/modules/gui/widgets/img/HorizontalSliderWithBrowser.png b/doc/source/modules/gui/widgets/img/HorizontalSliderWithBrowser.png
index d64b2df..8cfdb25 100644
--- a/doc/source/modules/gui/widgets/img/HorizontalSliderWithBrowser.png
+++ b/doc/source/modules/gui/widgets/img/HorizontalSliderWithBrowser.png
Binary files differ
diff --git a/doc/source/modules/gui/widgets/img/PeriodicCombo.png b/doc/source/modules/gui/widgets/img/PeriodicCombo.png
index e0b40c2..4a93e86 100644
--- a/doc/source/modules/gui/widgets/img/PeriodicCombo.png
+++ b/doc/source/modules/gui/widgets/img/PeriodicCombo.png
Binary files differ
diff --git a/doc/source/modules/gui/widgets/img/PeriodicList.png b/doc/source/modules/gui/widgets/img/PeriodicList.png
index d1e540b..42f432e 100644
--- a/doc/source/modules/gui/widgets/img/PeriodicList.png
+++ b/doc/source/modules/gui/widgets/img/PeriodicList.png
Binary files differ
diff --git a/doc/source/modules/gui/widgets/img/PeriodicTable.png b/doc/source/modules/gui/widgets/img/PeriodicTable.png
index c06dded..ce52262 100644
--- a/doc/source/modules/gui/widgets/img/PeriodicTable.png
+++ b/doc/source/modules/gui/widgets/img/PeriodicTable.png
Binary files differ
diff --git a/doc/source/modules/gui/widgets/img/RangeSlider.png b/doc/source/modules/gui/widgets/img/RangeSlider.png
index b068c6f..f552fb3 100644
--- a/doc/source/modules/gui/widgets/img/RangeSlider.png
+++ b/doc/source/modules/gui/widgets/img/RangeSlider.png
Binary files differ
diff --git a/doc/source/modules/gui/widgets/img/TableWidget.png b/doc/source/modules/gui/widgets/img/TableWidget.png
index a0f819f..b6a4965 100644
--- a/doc/source/modules/gui/widgets/img/TableWidget.png
+++ b/doc/source/modules/gui/widgets/img/TableWidget.png
Binary files differ
diff --git a/doc/source/modules/gui/widgets/img/ThreadPoolPushButton.png b/doc/source/modules/gui/widgets/img/ThreadPoolPushButton.png
index 4710d16..18a7416 100644
--- a/doc/source/modules/gui/widgets/img/ThreadPoolPushButton.png
+++ b/doc/source/modules/gui/widgets/img/ThreadPoolPushButton.png
Binary files differ
diff --git a/doc/source/modules/gui/widgets/img/WaitingPushButton.png b/doc/source/modules/gui/widgets/img/WaitingPushButton.png
index fa1d51a..4fd3e3c 100644
--- a/doc/source/modules/gui/widgets/img/WaitingPushButton.png
+++ b/doc/source/modules/gui/widgets/img/WaitingPushButton.png
Binary files differ
diff --git a/doc/source/modules/image/index.rst b/doc/source/modules/image/index.rst
index 477cc9f..cf4867b 100644
--- a/doc/source/modules/image/index.rst
+++ b/doc/source/modules/image/index.rst
@@ -12,4 +12,6 @@
marchingsquares.rst
shapes.rst
sift.rst
+ projection.rst
backprojection.rst
+ reconstruction.rst
diff --git a/doc/source/modules/image/shapes.rst b/doc/source/modules/image/shapes.rst
index a20b0cd..be51975 100644
--- a/doc/source/modules/image/shapes.rst
+++ b/doc/source/modules/image/shapes.rst
@@ -2,7 +2,7 @@
.. currentmodule:: silx.image
:mod:`shapes`: 2D shapes drawing
----------------------------------
+--------------------------------
.. automodule:: silx.image.shapes
- :members: circle_fill, draw_line, polygon_fill_mask, Polygon
+ :members: circle_fill, ellipse_fill, draw_line, polygon_fill_mask, Polygon
diff --git a/doc/source/modules/opencl/convolution.rst b/doc/source/modules/opencl/convolution.rst
new file mode 100644
index 0000000..225d016
--- /dev/null
+++ b/doc/source/modules/opencl/convolution.rst
@@ -0,0 +1,10 @@
+
+.. currentmodule:: silx.opencl
+
+:mod:`convolution`: Convolution
+-------------------------------
+
+.. automodule:: silx.opencl.convolution
+ :members: Convolution, gaussian_kernel
+ :show-inheritance:
+ :undoc-members:
diff --git a/doc/source/modules/opencl/index.rst b/doc/source/modules/opencl/index.rst
index e17eecb..ef8b9e6 100644
--- a/doc/source/modules/opencl/index.rst
+++ b/doc/source/modules/opencl/index.rst
@@ -10,6 +10,10 @@
sift/index.rst
fbp.rst
+ sinofilter.rst
+ processing.rst
+ convolution.rst
+ statistics.rst
medfilt.rst
codec_cbf.rst
diff --git a/doc/source/modules/opencl/processing.rst b/doc/source/modules/opencl/processing.rst
new file mode 100644
index 0000000..a246cc6
--- /dev/null
+++ b/doc/source/modules/opencl/processing.rst
@@ -0,0 +1,10 @@
+
+.. currentmodule:: silx.opencl
+
+:mod:`processing`: Processing
+-------------------------------
+
+.. automodule:: silx.opencl.processing
+ :members: OpenclProcessing, KernelContainer
+ :show-inheritance:
+ :undoc-members:
diff --git a/doc/source/modules/opencl/sinofilter.rst b/doc/source/modules/opencl/sinofilter.rst
new file mode 100644
index 0000000..81fe100
--- /dev/null
+++ b/doc/source/modules/opencl/sinofilter.rst
@@ -0,0 +1,9 @@
+
+.. currentmodule:: silx.opencl
+
+:mod:`sinofilter`: Sinogram filtering.
+--------------------------------------------------
+
+.. automodule:: silx.opencl.sinofilter
+ :members:
+ :show-inheritance:
diff --git a/doc/source/modules/opencl/statistics.rst b/doc/source/modules/opencl/statistics.rst
new file mode 100644
index 0000000..0db7566
--- /dev/null
+++ b/doc/source/modules/opencl/statistics.rst
@@ -0,0 +1,10 @@
+
+.. currentmodule:: silx.opencl
+
+:mod:`statistics`: Statistics
+-------------------------------
+
+.. automodule:: silx.opencl.statistics
+ :members: Statistics
+ :show-inheritance:
+ :undoc-members:
diff --git a/doc/source/sample_code/img/plot3dUpdateScatterFromThread.png b/doc/source/sample_code/img/plot3dUpdateScatterFromThread.png
new file mode 100644
index 0000000..acd1c58
--- /dev/null
+++ b/doc/source/sample_code/img/plot3dUpdateScatterFromThread.png
Binary files differ
diff --git a/doc/source/sample_code/index.rst b/doc/source/sample_code/index.rst
index a5cbf11..409391f 100644
--- a/doc/source/sample_code/index.rst
+++ b/doc/source/sample_code/index.rst
@@ -25,8 +25,7 @@ All sample codes can be downloaded as a zip file: |sample_code_archive|.
- Description
* - :download:`icons.py <../../../examples/icons.py>`
- .. image:: img/icons.png
- :height: 150px
- :align: center
+ :width: 150px
- Display icons and animated icons provided by silx.
:mod:`silx.gui.data` and :mod:`silx.gui.hdf5`
@@ -41,18 +40,15 @@ All sample codes can be downloaded as a zip file: |sample_code_archive|.
- Description
* - :download:`customHdf5TreeModel.py <../../../examples/customHdf5TreeModel.py>`
- .. image:: img/customHdf5TreeModel.png
- :height: 150px
- :align: center
+ :width: 150px
- Qt Hdf5 widget examples
* - :download:`customDataView.py <../../../examples/customDataView.py>`
- .. image:: img/customDataView.png
- :height: 150px
- :align: center
+ :width: 150px
- Qt data view example
* - :download:`hdf5widget.py <../../../examples/hdf5widget.py>`
- .. image:: img/hdf5widget.png
- :height: 150px
- :align: center
+ :width: 150px
- Qt Hdf5 widget examples
.. note:: This module has a dependency on the `h5py <http://www.h5py.org/>`_
@@ -71,8 +67,7 @@ All sample codes can be downloaded as a zip file: |sample_code_archive|.
- Description
* - :download:`fileDialog.py <../../../examples/fileDialog.py>`
- .. image:: img/fileDialog.png
- :height: 150px
- :align: center
+ :width: 150px
- Example for the use of the ImageFileDialog.
:mod:`silx.gui.widgets`
@@ -87,14 +82,13 @@ All sample codes can be downloaded as a zip file: |sample_code_archive|.
- Description
* - :download:`periodicTable.py <../../../examples/periodicTable.py>`
- .. image:: img/periodicTable.png
- :height: 150px
+ :width: 150px
:align: center
- This script is a simple example of how to use the periodic table widgets,
select elements and connect signals.
* - :download:`simplewidget.py <../../../examples/simplewidget.py>`
- .. image:: img/simplewidget.png
- :height: 150px
- :align: center
+ :width: 150px
- This script shows a gallery of simple widgets provided by silx.
It shows the following widgets:
@@ -117,8 +111,7 @@ Widgets
- Description
* - :download:`imageview.py <../../../examples/imageview.py>`
- .. image:: img/imageview.png
- :height: 150px
- :align: center
+ :width: 150px
- Example to show the use of :mod:`~silx.gui.plot.ImageView` widget.
It can be used to open an EDF or TIFF file from the shell command line.
@@ -134,14 +127,12 @@ Widgets
``./bootstrap.py python examples/imageview.py <file to open>``
* - :download:`stackView.py <../../../examples/stackView.py>`
- .. image:: img/stackView.png
- :height: 150px
- :align: center
+ :width: 150px
- This script is a simple example to illustrate how to use the
:mod:`~silx.gui.plot.StackView` widget.
* - :download:`colormapDialog.py <../../../examples/colormapDialog.py>`
- .. image:: img/colormapDialog.png
- :height: 150px
- :align: center
+ :width: 150px
- This script shows the features of a :mod:`~silx.gui.dialog.ColormapDialog`.
:class:`silx.gui.plot.actions.PlotAction`
@@ -158,16 +149,14 @@ Sample code that adds buttons to the toolbar of a silx plot widget.
- Description
* - :download:`plotClearAction.py <../../../examples/plotClearAction.py>`
- .. image:: img/plotClearAction.png
- :height: 150px
- :align: center
+ :width: 150px
- This script shows how to create a minimalistic
:class:`~silx.gui.plot.actions.PlotAction` that clear the plot.
This illustrates how to add more buttons in a plot widget toolbar.
* - :download:`shiftPlotAction.py <../../../examples/shiftPlotAction.py>`
- .. image:: img/shiftPlotAction.png
- :height: 150px
- :align: center
+ :width: 150px
- This script is a simple (trivial) example of how to create a :class:`~silx.gui.plot.PlotWindow`,
create a custom :class:`~silx.gui.plot.actions.PlotAction` and add it to the toolbar.
@@ -176,8 +165,7 @@ Sample code that adds buttons to the toolbar of a silx plot widget.
* - :download:`fftPlotAction.py <../../../examples/fftPlotAction.py>`,
:download:`fft.png <../../../examples/fft.png>`
- .. image:: img/fftPlotAction.png
- :height: 150px
- :align: center
+ :width: 150px
- This script is a simple example of how to create a :class:`~silx.gui.plot.PlotWindow`
with a custom :class:`~silx.gui.plot.actions.PlotAction` added to the toolbar.
@@ -206,8 +194,7 @@ Sample code that adds specific tools or functions to plot widgets.
- Description
* - :download:`plotWidget.py <../../../examples/plotWidget.py>`
- .. image:: img/plotWidget.png
- :height: 150px
- :align: center
+ :width: 150px
- This script shows how to create a custom window around a PlotWidget.
It subclasses :class:`QMainWindow`, uses a :class:`~silx.gui.plot.PlotWidget`
@@ -220,8 +207,7 @@ Sample code that adds specific tools or functions to plot widgets.
- :class:`silx.gui.plot.ColorBar.ColorBarWidget`
* - :download:`plotContextMenu.py <../../../examples/plotContextMenu.py>`
- .. image:: img/plotContextMenu.png
- :height: 150px
- :align: center
+ :width: 150px
- This script illustrates the addition of a context menu to a
:class:`~silx.gui.plot.PlotWidget`.
@@ -237,20 +223,17 @@ Sample code that adds specific tools or functions to plot widgets.
For more information on context menus, see Qt documentation.
* - :download:`plotItemsSelector.py <../../../examples/plotItemsSelector.py>`
- .. image:: img/plotItemsSelector.png
- :height: 150px
- :align: center
+ :width: 150px
- This example illustrates how to use a :class:`ItemsSelectionDialog` widget
associated with a :class:`~silx.gui.plot.PlotWidget`
* - :download:`plotLimits.py <../../../examples/plotLimits.py>`
- .. image:: img/plotLimits.png
- :height: 150px
- :align: center
+ :width: 150px
- This script is an example to illustrate how to use axis synchronization
tool.
* - :download:`plotUpdateCurveFromThread.py <../../../examples/plotUpdateCurveFromThread.py>`
- .. image:: img/plotUpdateCurveFromThread.png
- :height: 150px
- :align: center
+ :width: 150px
- This script illustrates the update of a :mod:`silx.gui.plot` widget from a thread.
The problem is that plot and GUI methods should be called from the main thread.
@@ -263,8 +246,7 @@ Sample code that adds specific tools or functions to plot widgets.
of a plot.
* - :download:`plotUpdateImageFromThread.py <../../../examples/plotUpdateImageFromThread.py>`
- .. image:: img/plotUpdateImageFromThread.png
- :height: 150px
- :align: center
+ :width: 150px
- This script illustrates the update of a :mod:`silx.gui.plot` widget from a thread.
The problem is that plot and GUI methods should be called from the main thread.
@@ -277,8 +259,7 @@ Sample code that adds specific tools or functions to plot widgets.
of a plot.
* - :download:`plotInteractiveImageROI.py <../../../examples/plotInteractiveImageROI.py>`
- .. image:: img/plotInteractiveImageROI.png
- :height: 150px
- :align: center
+ :width: 150px
- This script illustrates image ROI selection in a :class:`~silx.gui.plot.PlotWidget`
It uses :class:`~silx.gui.plot.tools.roi.RegionOfInterestManager` and
@@ -286,8 +267,7 @@ Sample code that adds specific tools or functions to plot widgets.
interactive selection and to display the list of selected ROIs.
* - :download:`printPreview.py <../../../examples/printPreview.py>`
- .. image:: img/printPreview.png
- :height: 150px
- :align: center
+ :width: 150px
- This script illustrates how to add a print preview tool button to any plot
widget inheriting :class:`~silx.gui.plot.PlotWidget`.
@@ -298,14 +278,12 @@ Sample code that adds specific tools or functions to plot widgets.
which allows them to send their content to the same print preview page.
* - :download:`scatterMask.py <../../../examples/scatterMask.py>`
- .. image:: img/scatterMask.png
- :height: 150px
- :align: center
+ :width: 150px
- This example demonstrates how to use ScatterMaskToolsWidget
and NamedScatterAlphaSlider with a PlotWidget.
* - :download:`syncaxis.py <../../../examples/syncaxis.py>`
- .. image:: img/syncaxis.png
- :height: 150px
- :align: center
+ :width: 150px
- This script is an example to illustrate how to use axis synchronization
tool.
@@ -323,8 +301,7 @@ Sample code that adds specific tools or functions to plot widgets.
- Description
* - :download:`plot3dContextMenu.py <../../../examples/plot3dContextMenu.py>`
- .. image:: img/plot3dContextMenu.png
- :height: 150px
- :align: center
+ :width: 150px
- This script adds a context menu to a :class:`silx.gui.plot3d.ScalarFieldView`.
This is done by adding a custom context menu to the :class:`Plot3DWidget`:
@@ -335,8 +312,7 @@ Sample code that adds specific tools or functions to plot widgets.
For more information on context menus, see Qt documentation.
* - :download:`viewer3DVolume.py <../../../examples/viewer3DVolume.py>`
- .. image:: img/viewer3DVolume.png
- :height: 150px
- :align: center
+ :width: 150px
- This script illustrates the use of :class:`silx.gui.plot3d.ScalarFieldView`.
It loads a 3D scalar data set from a file and displays iso-surfaces and
@@ -344,8 +320,7 @@ Sample code that adds specific tools or functions to plot widgets.
It can also be started without providing a file.
* - :download:`plot3dSceneWindow.py <../../../examples/plot3dSceneWindow.py>`
- .. image:: img/plot3dSceneWindow.png
- :height: 150px
- :align: center
+ :width: 150px
- This script displays the different items of :class:`~silx.gui.plot3d.SceneWindow`.
It shows the different visualizations of :class:`~silx.gui.plot3d.SceneWindow`
@@ -359,6 +334,20 @@ Sample code that adds specific tools or functions to plot widgets.
- 3D scatter plot
- 3D scalar field with iso-surface and cutting plane.
- A clipping plane.
+ * - :download:`plot3dUpdateScatterFromThread.py <../../../examples/plot3dUpdateScatterFromThread.py>`
+ - .. image:: img/plot3dUpdateScatterFromThread.png
+ :width: 150px
+ - This script illustrates the update of a
+ :class:`~silx.gui.plot3d.SceneWindow.SceneWindow` widget from a thread.
+
+ The problem is that GUI methods should be called from the main thread.
+ To safely update the scene from another thread, one need to execute the update
+ asynchronously in the main thread.
+ In this example, this is achieved with
+ :func:`~silx.gui.utils.concurrent.submitToQtMainThread`.
+
+ In this example a thread calls submitToQtMainThread to append data to a 3D scatter.
+
:mod:`silx.io` sample code
++++++++++++++++++++++++++
diff --git a/examples/compareImages.py b/examples/compareImages.py
index 94f68a0..623216a 100644
--- a/examples/compareImages.py
+++ b/examples/compareImages.py
@@ -30,19 +30,18 @@ import sys
import logging
import numpy
import argparse
+import os
import silx.io
from silx.gui import qt
import silx.test.utils
+from silx.io.url import DataUrl
from silx.gui.plot.CompareImages import CompareImages
+from silx.gui.widgets.UrlSelectionTable import UrlSelectionTable
_logger = logging.getLogger(__name__)
-try:
- import fabio
-except ImportError:
- _logger.debug("Backtrace", exc_info=True)
- fabio = None
+import fabio
try:
import PIL
@@ -51,6 +50,70 @@ except ImportError:
PIL = None
+class CompareImagesSelection(qt.QMainWindow):
+ def __init__(self, backend):
+ qt.QMainWindow.__init__(self, parent=None)
+ self._plot = CompareImages(parent=self, backend=backend)
+
+ self._selectionTable = UrlSelectionTable(parent=self)
+ self._dockWidgetMenu = qt.QDockWidget(parent=self)
+ self._dockWidgetMenu.layout().setContentsMargins(0, 0, 0, 0)
+ self._dockWidgetMenu.setFeatures(qt.QDockWidget.DockWidgetMovable)
+ self._dockWidgetMenu.setWidget(self._selectionTable)
+ self.addDockWidget(qt.Qt.LeftDockWidgetArea, self._dockWidgetMenu)
+
+ self.setCentralWidget(self._plot)
+
+ self._selectionTable.sigImageAChanged.connect(self._updateImageA)
+ self._selectionTable.sigImageBChanged.connect(self._updateImageB)
+
+ def setUrls(self, urls):
+ for url in urls:
+ self._selectionTable.addUrl(url)
+
+ def setFiles(self, files):
+ urls = list()
+ for _file in files:
+ if os.path.isfile(_file):
+ urls.append(DataUrl(file_path=_file, scheme=None))
+ urls.sort(key=lambda url: url.path())
+ window.setUrls(urls)
+ window._selectionTable.setSelection(url_img_a=urls[0].path(),
+ url_img_b=urls[1].path())
+
+ def clear(self):
+ self._plot.clear()
+ self._selectionTable.clear()
+
+ def _updateImageA(self, urlpath):
+ self._updateImage(urlpath, self._plot.setImage1)
+
+ def _updateImage(self, urlpath, fctptr):
+ def getData():
+ _url = silx.io.url.DataUrl(path=urlpath)
+ for scheme in ('silx', 'fabio'):
+ try:
+ dataImg = silx.io.utils.get_data(
+ silx.io.url.DataUrl(file_path=_url.file_path(),
+ data_slice=_url.data_slice(),
+ data_path=_url.data_path(),
+ scheme=scheme))
+ except:
+ _logger.debug("Error while loading image with %s" % scheme,
+ exc_info=True)
+ else:
+ # TODO: check is an image
+ return dataImg
+ return None
+
+ data = getData()
+ if data is not None:
+ fctptr(data)
+
+ def _updateImageB(self, urlpath):
+ self._updateImage(urlpath, self._plot.setImage2)
+
+
def createTestData():
data = numpy.arange(100 * 100)
data = (data % 100) / 5.0
@@ -68,14 +131,10 @@ def loadImage(filename):
except Exception:
_logger.debug("Error while loading image with silx.io", exc_info=True)
- if fabio is None and PIL is None:
- raise ImportError("fabio nor PIL are available")
-
- if fabio is not None:
- try:
- return fabio.open(filename).data
- except Exception:
- _logger.debug("Error while loading image with fabio", exc_info=True)
+ try:
+ return fabio.open(filename).data
+ except Exception:
+ _logger.debug("Error while loading image with fabio", exc_info=True)
if PIL is not None:
try:
@@ -128,22 +187,25 @@ if __name__ == "__main__":
if options.debug:
logging.root.setLevel(logging.DEBUG)
- if options.testdata:
- _logger.info("Generate test data")
- data1, data2 = createTestData()
- else:
- if len(options.files) != 2:
- raise Exception("Expected 2 images to compare them")
- data1 = loadImage(options.files[0])
- data2 = loadImage(options.files[1])
-
if options.use_opengl_plot:
backend = "gl"
else:
backend = "mpl"
app = qt.QApplication([])
- window = CompareImages(backend=backend)
- window.setData(data1, data2)
+ if options.testdata or len(options.files) == 2:
+ if options.testdata:
+ _logger.info("Generate test data")
+ data1, data2 = createTestData()
+ else:
+ data1 = loadImage(options.files[0])
+ data2 = loadImage(options.files[1])
+ window = CompareImages(backend=backend)
+ window.setData(data1, data2)
+ else:
+ data = options.files
+ window = CompareImagesSelection(backend=backend)
+ window.setFiles(options.files)
+
window.setVisible(True)
app.exec_()
diff --git a/examples/customDataView.py b/examples/customDataView.py
index 6db5c3e..33662e8 100644
--- a/examples/customDataView.py
+++ b/examples/customDataView.py
@@ -2,7 +2,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -26,6 +26,7 @@
"""Qt data view example
"""
+import enum
import logging
import sys
@@ -36,7 +37,6 @@ _logger = logging.getLogger("customDataView")
from silx.gui import qt
from silx.gui.data.DataViewerFrame import DataViewerFrame
from silx.gui.data.DataViews import DataView
-from silx.third_party import enum
class Color(enum.Enum):
diff --git a/examples/dropZones.py b/examples/dropZones.py
new file mode 100644
index 0000000..d0d16b5
--- /dev/null
+++ b/examples/dropZones.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""
+Example of drop zone supporting application/x-silx-uri
+"""
+
+from __future__ import absolute_import
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "25/01/2019"
+
+import logging
+import silx.io
+from silx.gui import qt
+from silx.gui.plot.PlotWidget import PlotWidget
+
+_logger = logging.getLogger(__name__)
+logging.basicConfig(level=logging.DEBUG)
+
+
+class DropPlotWidget(PlotWidget):
+
+ def __init__(self, parent=None, backend=None):
+ PlotWidget.__init__(self, parent=parent, backend=backend)
+ self.setAcceptDrops(True)
+
+ def dragEnterEvent(self, event):
+ if event.mimeData().hasFormat("application/x-silx-uri"):
+ event.acceptProposedAction()
+
+ def dropEvent(self, event):
+ byteString = event.mimeData().data("application/x-silx-uri")
+ silxUrl = byteString.data().decode("utf-8")
+ with silx.io.open(silxUrl) as h5:
+ if silx.io.is_dataset(h5):
+ dataset = h5[...]
+ else:
+ _logger.error("Unsupported URI")
+ dataset = None
+
+ if dataset is not None:
+ if dataset.ndim == 1:
+ self.clear()
+ self.addCurve(y=dataset, x=range(dataset.size))
+ event.acceptProposedAction()
+ elif dataset.ndim == 2:
+ self.clear()
+ self.addImage(data=dataset)
+ event.acceptProposedAction()
+ else:
+ _logger.error("Unsupported dataset")
+
+
+class DropLabel(qt.QLabel):
+
+ def __init__(self, parent=None, backend=None):
+ qt.QLabel.__init__(self)
+ self.setAcceptDrops(True)
+ self.setText("Drop something here")
+
+ def dragEnterEvent(self, event):
+ if event.mimeData().hasFormat("application/x-silx-uri"):
+ event.acceptProposedAction()
+
+ def dropEvent(self, event):
+ byteString = event.mimeData().data("application/x-silx-uri")
+ silxUrl = byteString.data().decode("utf-8")
+ url = silx.io.url.DataUrl(silxUrl)
+ self.setText(url.path())
+
+ toolTipTemplate = ("<html><ul>"
+ "<li><b>file_path</b>: {file_path}</li>"
+ "<li><b>data_path</b>: {data_path}</li>"
+ "<li><b>data_slice</b>: {data_slice}</li>"
+ "<li><b>scheme</b>: {scheme}</li>"
+ "</html>"
+ "</ul></html>"
+ )
+
+ toolTip = toolTipTemplate.format(
+ file_path=url.file_path(),
+ data_path=url.data_path(),
+ data_slice=url.data_slice(),
+ scheme=url.scheme())
+
+ self.setToolTip(toolTip)
+ event.acceptProposedAction()
+
+
+class DropExample(qt.QMainWindow):
+
+ def __init__(self, parent=None):
+ super(DropExample, self).__init__(parent)
+ centralWidget = qt.QWidget(self)
+ layout = qt.QVBoxLayout()
+ centralWidget.setLayout(layout)
+ layout.addWidget(DropPlotWidget(parent=self))
+ layout.addWidget(DropLabel(parent=self))
+ self.setCentralWidget(centralWidget)
+
+
+def main():
+ app = qt.QApplication([])
+ example = DropExample()
+ example.show()
+ app.exec_()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/fileDialog.py b/examples/fileDialog.py
index 9730b9a..82e6798 100644
--- a/examples/fileDialog.py
+++ b/examples/fileDialog.py
@@ -2,7 +2,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -33,12 +33,12 @@ __authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "14/02/2018"
+import enum
import logging
from silx.gui import qt
from silx.gui.dialog.ImageFileDialog import ImageFileDialog
from silx.gui.dialog.DataFileDialog import DataFileDialog
import silx.io
-from silx.third_party import enum
logging.basicConfig(level=logging.DEBUG)
diff --git a/examples/hdf5widget.py b/examples/hdf5widget.py
index bf92d4e..c344bec 100755
--- a/examples/hdf5widget.py
+++ b/examples/hdf5widget.py
@@ -33,7 +33,9 @@
import logging
import sys
import tempfile
+
import numpy
+import six
logging.basicConfig()
_logger = logging.getLogger("hdf5widget")
@@ -50,15 +52,12 @@ import h5py
import silx.gui.hdf5
import silx.utils.html
-from silx.third_party import six
from silx.gui import qt
from silx.gui.data.DataViewerFrame import DataViewerFrame
from silx.gui.widgets.ThreadPoolPushButton import ThreadPoolPushButton
-try:
- import fabio
-except ImportError:
- fabio = None
+import fabio
+
_file_cache = {}
@@ -713,26 +712,25 @@ class Hdf5TreeViewExample(qt.QMainWindow):
content.layout().addStretch(1)
- if fabio is not None:
- content = qt.QGroupBox("Create EDF", panel)
- content.setLayout(qt.QVBoxLayout())
- panel.layout().addWidget(content)
+ content = qt.QGroupBox("Create EDF", panel)
+ content.setLayout(qt.QVBoxLayout())
+ panel.layout().addWidget(content)
- combo = qt.QComboBox()
- combo.addItem("Containing all types", get_edf_with_all_types)
- combo.addItem("Containing 100000 datasets", get_edf_with_100000_frames)
- combo.activated.connect(self.__edfComboChanged)
- content.layout().addWidget(combo)
+ combo = qt.QComboBox()
+ combo.addItem("Containing all types", get_edf_with_all_types)
+ combo.addItem("Containing 100000 datasets", get_edf_with_100000_frames)
+ combo.activated.connect(self.__edfComboChanged)
+ content.layout().addWidget(combo)
- button = ThreadPoolPushButton(content, text="Create")
- button.setCallable(combo.itemData(combo.currentIndex()))
- button.succeeded.connect(self.__fileCreated)
- content.layout().addWidget(button)
+ button = ThreadPoolPushButton(content, text="Create")
+ button.setCallable(combo.itemData(combo.currentIndex()))
+ button.succeeded.connect(self.__fileCreated)
+ content.layout().addWidget(button)
- self.__edfCombo = combo
- self.__createEdfButton = button
+ self.__edfCombo = combo
+ self.__createEdfButton = button
- content.layout().addStretch(1)
+ content.layout().addStretch(1)
option = qt.QGroupBox("Tree options", panel)
option.setLayout(qt.QVBoxLayout())
diff --git a/examples/imageview.py b/examples/imageview.py
index 37d1857..71bde70 100755
--- a/examples/imageview.py
+++ b/examples/imageview.py
@@ -42,7 +42,7 @@ from __future__ import division
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "18/10/2016"
+__date__ = "08/11/2018"
import logging
from silx.gui.plot.ImageView import ImageViewMainWindow
@@ -118,8 +118,7 @@ def main(argv=None):
if args.log: # Use log normalization by default
colormap = mainWindow.getDefaultColormap()
- colormap['normalization'] = 'log'
- mainWindow.setColormap(colormap)
+ colormap.setNormalization(colormap.LOGARITHM)
mainWindow.setImage(data,
origin=args.origin,
diff --git a/examples/plot3dSceneWindow.py b/examples/plot3dSceneWindow.py
index cf6f209..1b2f808 100644
--- a/examples/plot3dSceneWindow.py
+++ b/examples/plot3dSceneWindow.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -51,13 +51,11 @@ import numpy
from silx.gui import qt
from silx.gui.plot3d.SceneWindow import SceneWindow, items
-from silx.gui.plot3d.tools.PositionInfoWidget import PositionInfoWidget
-from silx.gui.widgets.BoxLayoutDockWidget import BoxLayoutDockWidget
SIZE = 1024
# Create QApplication
-qapp = qt.QApplication([])
+qapp = qt.QApplication.instance() or qt.QApplication([])
# Create a SceneWindow widget
window = SceneWindow()
@@ -69,20 +67,13 @@ sceneWidget.setForegroundColor((1., 1., 1., 1.))
sceneWidget.setTextColor((0.1, 0.1, 0.1, 1.))
-# Add PositionInfoWidget to display picking info
-positionInfo = PositionInfoWidget()
-positionInfo.setSceneWidget(sceneWidget)
-dock = BoxLayoutDockWidget()
-dock.setWindowTitle("Selection Info")
-dock.setWidget(positionInfo)
-window.addDockWidget(qt.Qt.BottomDockWidgetArea, dock)
-
# 2D Image ###
# Add a dummy RGBA image
img = numpy.random.random(3 * SIZE ** 2).reshape(SIZE, SIZE, 3) # Dummy image
imageRgba = sceneWidget.addImage(img) # Add ImageRgba item to the scene
+imageRgba.setLabel('Random RGBA image') # Set name displayed in parameter tree
# Set imageRgba transform
imageRgba.setTranslation(SIZE*.15, SIZE*.15, 0.) # Translate the image
diff --git a/examples/plot3dUpdateScatterFromThread.py b/examples/plot3dUpdateScatterFromThread.py
new file mode 100644
index 0000000..9c2213f
--- /dev/null
+++ b/examples/plot3dUpdateScatterFromThread.py
@@ -0,0 +1,176 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This script illustrates the update of a
+:class:`~silx.gui.plot3d.SceneWindow.SceneWindow` widget from a thread.
+
+The problem is that GUI methods should be called from the main thread.
+To safely update the scene from another thread, one need to execute the update
+asynchronously in the main thread.
+In this example, this is achieved with
+:func:`~silx.gui.utils.concurrent.submitToQtMainThread`.
+
+In this example a thread calls submitToQtMainThread to append data to a 3D scatter.
+"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "08/03/2019"
+
+
+import threading
+import time
+
+import numpy
+
+from silx.gui import qt
+from silx.gui.utils import concurrent
+from silx.gui.plot3d.SceneWindow import SceneWindow
+from silx.gui.plot3d import items
+
+
+MAX_NUMBER_OF_POINTS = 10**6
+
+
+class UpdateScatterThread(threading.Thread):
+ """Thread updating the scatter 3D item data
+
+ :param ~silx.gui.plot3d.items.Scatter3D scatter3d: 3D scatter to update.
+ """
+
+ def __init__(self, scatter3d):
+ self.scatter3d = scatter3d
+ self.running = False
+ self.future_result = None
+ super(UpdateScatterThread, self).__init__()
+
+ def start(self):
+ """Start the update thread"""
+ self.running = True
+ super(UpdateScatterThread, self).start()
+
+ def _appendScatterData(self, x, y, z, value):
+ """Add some data points to the Scatter3D item.
+
+ This method MUST be called in the Qt main thread.
+
+ :param numpy.ndarray x:
+ :param numpy.ndarray y:
+ :param numpy.ndarray z:
+ :param numpy.ndarray value:
+ """
+ # use copy=False to avoid useless copy of numpy arrays
+ curX, curY, curZ, curValue = self.scatter3d.getData(copy=False)
+
+ x = numpy.append(curX, x)
+ y = numpy.append(curY, y)
+ z = numpy.append(curZ, z)
+ value = numpy.append(curValue, value)
+
+ # Update data
+ self.scatter3d.setData(x, y, z, value, copy=False)
+
+ def run(self):
+ """Method implementing thread loop that updates the scatter data
+
+ It produces adds scatter points every 10 ms or so, up to 1 million.
+ """
+ count = 0 # Number of data points currently rendered
+
+ # Init arrays that accumulate scatter points
+ x = numpy.array((), dtype=numpy.float32)
+ y = numpy.array((), dtype=numpy.float32)
+ z = numpy.array((), dtype=numpy.float32)
+ value = numpy.array((), dtype=numpy.float32)
+
+ while self.running:
+ time.sleep(0.01)
+
+ # Generate new data points
+ inclination = numpy.random.random(1000).astype(numpy.float32) * numpy.pi
+ azimuth = numpy.random.random(1000).astype(numpy.float32) * 2. * numpy.pi
+ radius = numpy.random.normal(loc=10., scale=.5, size=1000)
+ newX = radius * numpy.sin(inclination) * numpy.cos(azimuth)
+ newY = radius * numpy.sin(inclination) * numpy.sin(azimuth)
+ newZ = radius * numpy.cos(inclination)
+ newValue = numpy.random.random(1000).astype(numpy.float32)
+
+ # Accumulate data points
+ x = numpy.append(x, newX)
+ y = numpy.append(y, newY)
+ z = numpy.append(z, newZ)
+ value = numpy.append(value, newValue)
+
+ # Only append data if the previous one has been added
+ if self.future_result is None or self.future_result.done():
+ if count > MAX_NUMBER_OF_POINTS:
+ # Restart a new scatter plot asyn
+ self.future_result = concurrent.submitToQtMainThread(
+ self.scatter3d.setData, x, y, z, value)
+
+ count = len(x)
+ else:
+ # Append data asynchronously
+ self.future_result = concurrent.submitToQtMainThread(
+ self._appendScatterData, x, y, z, value)
+
+ count += len(x)
+
+ # Reset accumulators
+ x = numpy.array((), dtype=numpy.float32)
+ y = numpy.array((), dtype=numpy.float32)
+ z = numpy.array((), dtype=numpy.float32)
+ value = numpy.array((), dtype=numpy.float32)
+
+ def stop(self):
+ """Stop the update thread"""
+ self.running = False
+ self.join(2)
+
+
+def main():
+ global app
+ app = qt.QApplication([])
+
+ # Create a SceneWindow
+ window = SceneWindow()
+ window.show()
+
+ sceneWidget = window.getSceneWidget()
+ scatter = items.Scatter3D()
+ scatter.setSymbol(',')
+ scatter.getColormap().setName('magma')
+ sceneWidget.addItem(scatter)
+
+ # Create the thread that calls submitToQtMainThread
+ updateThread = UpdateScatterThread(scatter)
+ updateThread.start() # Start updating the plot
+
+ app.exec_()
+
+ updateThread.stop() # Stop updating the plot
+
+
+if __name__ == '__main__':
+ main()
diff --git a/examples/plotInteractiveImageROI.py b/examples/plotInteractiveImageROI.py
index d45bdf5..e06db89 100644
--- a/examples/plotInteractiveImageROI.py
+++ b/examples/plotInteractiveImageROI.py
@@ -39,14 +39,14 @@ from silx.gui.plot import Plot2D
from silx.gui.plot.tools.roi import RegionOfInterestManager
from silx.gui.plot.tools.roi import RegionOfInterestTableWidget
from silx.gui.plot.items.roi import RectangleROI
+from silx.gui.plot.items import LineMixIn, SymbolMixIn
def dummy_image():
"""Create a dummy image"""
x = numpy.linspace(-1.5, 1.5, 1024)
xv, yv = numpy.meshgrid(x, x)
- signal = numpy.exp(- (xv ** 2 / 0.15 ** 2
- + yv ** 2 / 0.25 ** 2))
+ signal = numpy.exp(- (xv ** 2 / 0.15 ** 2 + yv ** 2 / 0.25 ** 2))
# add noise
signal += 0.3 * numpy.random.random(size=signal.shape)
return signal
@@ -54,8 +54,12 @@ def dummy_image():
app = qt.QApplication([]) # Start QApplication
+backend = "matplotlib"
+if "--opengl" in sys.argv:
+ backend = "opengl"
+
# Create the plot widget and add an image
-plot = Plot2D()
+plot = Plot2D(backend=backend)
plot.getDefaultColormap().setName('viridis')
plot.addImage(dummy_image())
@@ -69,6 +73,12 @@ def updateAddedRegionOfInterest(roi):
"""Called for each added region of interest: set the name"""
if roi.getLabel() == '':
roi.setLabel('ROI %d' % len(roiManager.getRois()))
+ if isinstance(roi, LineMixIn):
+ roi.setLineWidth(2)
+ roi.setLineStyle('--')
+ if isinstance(roi, SymbolMixIn):
+ roi.setSymbol('o')
+ roi.setSymbolSize(5)
roiManager.sigRoiAdded.connect(updateAddedRegionOfInterest)
@@ -99,6 +109,7 @@ widget.setLayout(layout)
layout.addWidget(roiToolbar)
layout.addWidget(roiTable)
+
def roiDockVisibilityChanged(visible):
"""Handle change of visibility of the roi dock widget
@@ -107,6 +118,7 @@ def roiDockVisibilityChanged(visible):
if not visible:
roiManager.stop()
+
dock = qt.QDockWidget('Image ROI')
dock.setWidget(widget)
dock.visibilityChanged.connect(roiDockVisibilityChanged)
diff --git a/examples/plotLimits.py b/examples/plotLimits.py
index 0a39bc6..c7cc7f5 100644
--- a/examples/plotLimits.py
+++ b/examples/plotLimits.py
@@ -23,8 +23,8 @@
# THE SOFTWARE.
#
# ###########################################################################*/
-"""This script is an example to illustrate how to use axis synchronization
-tool.
+"""This script is an example to illustrate how to set range constraints on
+plot axes.
"""
from silx.gui import qt
@@ -37,7 +37,7 @@ class ConstrainedViewPlot(qt.QMainWindow):
def __init__(self):
qt.QMainWindow.__init__(self)
- self.setWindowTitle("Plot with synchronized axes")
+ self.setWindowTitle("Plot with constrained axes")
widget = qt.QWidget(self)
self.setCentralWidget(widget)
diff --git a/examples/plotStats.py b/examples/plotStats.py
index fff7585..3929697 100644
--- a/examples/plotStats.py
+++ b/examples/plotStats.py
@@ -2,7 +2,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -43,9 +43,49 @@ __date__ = "24/07/2018"
from silx.gui import qt
+from silx.gui.colors import Colormap
from silx.gui.plot import Plot1D
from silx.gui.plot.stats.stats import StatBase
+from silx.gui.utils import concurrent
+import random
+import threading
+import argparse
import numpy
+import time
+
+
+class UpdateThread(threading.Thread):
+ """Thread updating the curve of a :class:`~silx.gui.plot.Plot1D`
+
+ :param plot1d: The Plot1D to update."""
+
+ def __init__(self, plot1d):
+ self.plot1d = plot1d
+ self.running = False
+ super(UpdateThread, self).__init__()
+
+ def start(self):
+ """Start the update thread"""
+ self.running = True
+ super(UpdateThread, self).start()
+
+ def run(self):
+ """Method implementing thread loop that updates the plot"""
+ while self.running:
+ time.sleep(1)
+ # Run plot update asynchronously
+ concurrent.submitToQtMainThread(
+ self.plot1d.addCurve,
+ numpy.arange(1000),
+ numpy.random.random(1000),
+ resetzoom=False,
+ legend=random.choice(('mycurve0', 'mycurve1'))
+ )
+
+ def stop(self):
+ """Stop the update thread"""
+ self.running = False
+ self.join(2)
class Integral(StatBase):
@@ -87,21 +127,27 @@ class COM(StatBase):
return comX, comY
-def main():
+def main(argv):
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ '--update-mode',
+ default='manual',
+ help='update mode to display (manual or auto)')
+
+ options = parser.parse_args(argv[1:])
+
app = qt.QApplication([])
plot = Plot1D()
- x = numpy.arange(21)
- y = numpy.arange(21)
- plot.addCurve(x=x, y=y, legend='myCurve')
- plot.addCurve(x=x, y=(y + 5), legend='myCurve2')
-
- plot.setActiveCurve('myCurve')
+ # Create the thread that calls submitToQtMainThread
+ updateThread = UpdateThread(plot)
+ updateThread.start() # Start updating the plot
plot.addScatter(x=[0, 2, 5, 5, 12, 20],
y=[2, 3, 4, 20, 15, 6],
value=[5, 6, 7, 10, 90, 20],
+ colormap=Colormap('viridis'),
legend='myScatter')
stats = [
@@ -111,11 +157,14 @@ def main():
]
plot.getStatsWidget().setStats(stats)
+ plot.getStatsWidget().setUpdateMode(options.update_mode)
+ plot.getStatsWidget().setDisplayOnlyActiveItem(False)
plot.getStatsWidget().parent().setVisible(True)
plot.show()
app.exec_()
+ updateThread.stop() # Stop updating the plot
if __name__ == '__main__':
- main()
+ main(sys.argv)
diff --git a/examples/plotWidget.py b/examples/plotWidget.py
index c8a90a5..af64afb 100644
--- a/examples/plotWidget.py
+++ b/examples/plotWidget.py
@@ -36,7 +36,7 @@ as its central widget and adds toolbars and a colorbar by using pluggable widget
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "05/09/2017"
+__date__ = "06/05/2019"
import numpy
@@ -68,15 +68,29 @@ class MyPlotWindow(qt.QMainWindow):
palette.setColor(qt.QPalette.Window, qt.Qt.white)
colorBar.setPalette(palette)
+ options = qt.QWidget(self)
+ layout = qt.QVBoxLayout(options)
+ button = qt.QPushButton("Show image", self)
+ button.clicked.connect(self.showImage)
+ layout.addWidget(button)
+ button = qt.QPushButton("Show scatter", self)
+ button.clicked.connect(self.showScatter)
+ layout.addWidget(button)
+ button = qt.QPushButton("Show delaunay", self)
+ button.clicked.connect(self.showDelaunay)
+ layout.addWidget(button)
+ layout.addStretch()
+
# Combine the ColorBarWidget and the PlotWidget as
# this QMainWindow's central widget
gridLayout = qt.QGridLayout()
gridLayout.setSpacing(0)
gridLayout.setContentsMargins(0, 0, 0, 0)
- gridLayout.addWidget(self._plot, 0, 0)
- gridLayout.addWidget(colorBar, 0, 1)
+ gridLayout.addWidget(options, 0, 0)
+ gridLayout.addWidget(self._plot, 0, 1)
+ gridLayout.addWidget(colorBar, 0, 2)
gridLayout.setRowStretch(0, 1)
- gridLayout.setColumnStretch(0, 1)
+ gridLayout.setColumnStretch(1, 1)
centralWidget = qt.QWidget(self)
centralWidget.setLayout(gridLayout)
self.setCentralWidget(centralWidget)
@@ -116,6 +130,44 @@ class MyPlotWindow(qt.QMainWindow):
"""Returns the PlotWidget contains in this window"""
return self._plot
+ def showImage(self):
+ plot = self.getPlotWidget()
+ plot.clear()
+ plot.getDefaultColormap().setName('viridis')
+
+ # Add an image to the plot
+ x = numpy.outer(
+ numpy.linspace(-10, 10, 200), numpy.linspace(-10, 5, 150))
+ image = numpy.sin(x) / x
+ plot.addImage(image)
+ plot.resetZoom()
+
+ def showScatter(self):
+ plot = self.getPlotWidget()
+ plot.clear()
+ plot.getDefaultColormap().setName('viridis')
+
+ nbPoints = 50
+ x = numpy.random.rand(nbPoints)
+ y = numpy.random.rand(nbPoints)
+ value = numpy.random.rand(nbPoints)
+ plot.addScatter(x=x, y=y, value=value)
+ plot.resetZoom()
+
+ def showDelaunay(self):
+ plot = self.getPlotWidget()
+ plot.clear()
+ plot.getDefaultColormap().setName('viridis')
+
+ nbPoints = 50
+ x = numpy.random.rand(nbPoints)
+ y = numpy.random.rand(nbPoints)
+ value = numpy.random.rand(nbPoints)
+ legend = plot.addScatter(x=x, y=y, value=value)
+ scatter = plot.getScatter(legend)
+ scatter.setVisualization("solid")
+ plot.resetZoom()
+
def main():
global app
@@ -125,17 +177,7 @@ def main():
window = MyPlotWindow()
window.setAttribute(qt.Qt.WA_DeleteOnClose)
window.show()
-
- # Change the default colormap
- plot = window.getPlotWidget()
- plot.getDefaultColormap().setName('viridis')
-
- # Add an image to the plot
- x = numpy.outer(
- numpy.linspace(-10, 10, 200), numpy.linspace(-10, 5, 150))
- image = numpy.sin(x) / x
- plot.addImage(image)
-
+ window.showImage()
app.exec_()
diff --git a/examples/printPreview.py b/examples/printPreview.py
index 7567adb..6de8209 100755
--- a/examples/printPreview.py
+++ b/examples/printPreview.py
@@ -42,22 +42,38 @@ from silx.gui import qt
from silx.gui.plot import PlotWidget
from silx.gui.plot import PrintPreviewToolButton
+
+class MyPrintPreviewButton(PrintPreviewToolButton.PrintPreviewToolButton):
+ """This class illustrates how to subclass PrintPreviewToolButton
+ to add a title and a comment."""
+ def getTitle(self):
+ return "Widget 1's plot"
+
+ def getCommentAndPosition(self):
+ legends = self.getPlot().getAllCurves(just_legend=True)
+ comment = "Curves displayed in widget 1:\n\t"
+ if legends:
+ comment += ", ".join(legends)
+ else:
+ comment += "none"
+ return comment, "CENTER"
+
+
app = qt.QApplication([])
x = numpy.arange(1000)
-# first widget has a standalone print preview action
+# first widget has a standalone preview action with custom title and comment
pw1 = PlotWidget()
pw1.setWindowTitle("Widget 1 with standalone print preview")
toolbar1 = qt.QToolBar(pw1)
-toolbutton1 = PrintPreviewToolButton.PrintPreviewToolButton(parent=toolbar1,
- plot=pw1)
+toolbutton1 = MyPrintPreviewButton(parent=toolbar1, plot=pw1)
pw1.addToolBar(toolbar1)
toolbar1.addWidget(toolbutton1)
pw1.show()
pw1.addCurve(x, numpy.tan(x * 2 * numpy.pi / 1000))
-# next two plots share a common print preview
+# next two plots share a common standard print preview
pw2 = PlotWidget()
pw2.setWindowTitle("Widget 2 with shared print preview")
toolbar2 = qt.QToolBar(pw2)
diff --git a/examples/syncPlotLocation.py b/examples/syncPlotLocation.py
new file mode 100644
index 0000000..55332bc
--- /dev/null
+++ b/examples/syncPlotLocation.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python
+# 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.
+#
+# ###########################################################################*/
+"""This script is an example to illustrate how to use axis synchronization
+tool.
+"""
+
+from silx.gui import qt
+from silx.gui.plot import Plot2D
+import numpy
+import silx.test.utils
+from silx.gui.plot.utils.axis import SyncAxes
+from silx.gui.colors import Colormap
+
+
+class SyncPlot(qt.QMainWindow):
+
+ def __init__(self):
+ qt.QMainWindow.__init__(self)
+ self.setWindowTitle("Plot with synchronized axes")
+ widget = qt.QWidget(self)
+ self.setCentralWidget(widget)
+
+ layout = qt.QGridLayout()
+ widget.setLayout(layout)
+
+ backend = "gl"
+ plots = []
+
+ data = numpy.arange(100 * 100)
+ data = (data % 100) / 5.0
+ data = numpy.sin(data)
+ data.shape = 100, 100
+
+ colormaps = ["gray", "red", "green", "blue"]
+ for i in range(2 * 2):
+ plot = Plot2D(parent=widget, backend=backend)
+ plot.setInteractiveMode('pan')
+ plot.setDefaultColormap(Colormap(colormaps[i]))
+ noisyData = silx.test.utils.add_gaussian_noise(data, mean=i / 10.0)
+ plot.addImage(noisyData)
+ plots.append(plot)
+
+ xAxis = [p.getXAxis() for p in plots]
+ yAxis = [p.getYAxis() for p in plots]
+
+ self.constraint1 = SyncAxes(xAxis,
+ syncLimits=False,
+ syncScale=True,
+ syncDirection=True,
+ syncCenter=True,
+ syncZoom=True)
+ self.constraint2 = SyncAxes(yAxis,
+ syncLimits=False,
+ syncScale=True,
+ syncDirection=True,
+ syncCenter=True,
+ syncZoom=True)
+
+ for i, plot in enumerate(plots):
+ if i % 2 == 0:
+ plot.setFixedWidth(400)
+ else:
+ plot.setFixedWidth(500)
+ if i // 2 == 0:
+ plot.setFixedHeight(400)
+ else:
+ plot.setFixedHeight(500)
+ layout.addWidget(plot, i // 2, i % 2)
+
+ def createCenteredLabel(self, text):
+ label = qt.QLabel(self)
+ label.setAlignment(qt.Qt.AlignCenter)
+ label.setText(text)
+ return label
+
+
+if __name__ == "__main__":
+ app = qt.QApplication([])
+ window = SyncPlot()
+ window.setAttribute(qt.Qt.WA_DeleteOnClose, True)
+ window.setVisible(True)
+ app.exec_()
diff --git a/examples/viewer3DVolume.py b/examples/viewer3DVolume.py
index d030fba..4de04f6 100644
--- a/examples/viewer3DVolume.py
+++ b/examples/viewer3DVolume.py
@@ -52,13 +52,7 @@ logging.basicConfig()
_logger = logging.getLogger(__name__)
-
-try:
- import h5py
-except ImportError:
- _logger.warning('h5py is not installed: HDF5 not supported')
- h5py = None
-
+import h5py
def load(filename):
"""Load 3D scalar field from file.
@@ -72,7 +66,7 @@ def load(filename):
if not os.path.isfile(filename.split('::')[0]):
raise IOError('No input file: %s' % filename)
- if h5py is not None and h5py.is_hdf5(filename.split('::')[0]):
+ if h5py.is_hdf5(filename.split('::')[0]):
if '::' not in filename:
raise ValueError(
'HDF5 path not provided: Use <filename>::<path> format')
diff --git a/package/debian8/control b/package/debian8/control
index d84a339..2e3fc58 100644
--- a/package/debian8/control
+++ b/package/debian8/control
@@ -5,64 +5,47 @@ Uploaders: Jerome Kieffer <jerome.kieffer@esrf.fr>,
Section: science
Priority: extra
Build-Depends: cython,
- cython-dbg,
cython3,
- cython3-dbg,
- libstdc++6-4.9-dbg,
libstdc++-4.9-dev,
libstdc++6,
debhelper (>=9.20150101+deb8u2),
dh-python,
python-all-dev,
- python-all-dbg,
python-numpy,
- python-numpy-dbg,
python-fabio,
- python-fabio-dbg,
python-h5py,
- python-h5py-dbg,
python-pyopencl,
- python-pyopencl-dbg,
python-mako,
python-matplotlib,
- python-matplotlib-dbg,
+ python-nbsphinx,
python-dateutil,
python-opengl,
python-pyqt5,
- python-pyqt5-dbg,
python-pyqt5.qtsvg,
- python-pyqt5.qtsvg-dbg,
python-pyqt5.qtopengl,
- python-pyqt5.qtopengl-dbg,
python-scipy,
- python-scipy-dbg,
+ python-setuptools,
+ python-six,
python-sphinx,
python-sphinxcontrib.programoutput,
python-enum34,
python-concurrent.futures,
python3-all-dev,
- python3-all-dbg,
python3-numpy,
- python3-numpy-dbg,
python3-fabio,
- python3-fabio-dbg,
python3-h5py,
- python3-h5py-dbg,
python3-pyopencl,
- python3-pyopencl-dbg,
python3-mako,
python3-matplotlib,
- python3-matplotlib-dbg,
+ python3-nbsphinx,
python3-dateutil,
python3-opengl,
python3-pyqt5,
- python3-pyqt5-dbg,
python3-pyqt5.qtsvg,
- python3-pyqt5.qtsvg-dbg,
python3-pyqt5.qtopengl,
- python3-pyqt5.qtopengl-dbg,
python3-scipy,
- python3-scipy-dbg,
+ python3-setuptools,
+ python3-six,
python3-sphinx,
python3-sphinxcontrib.programoutput,
help2man,
@@ -104,6 +87,7 @@ Depends: ${misc:Depends},
python-pyqt5.qtsvg,
python-pyqt5.qtopengl,
python-scipy,
+ python-setuptools,
python-six,
python-enum34,
python-concurrent.futures,
@@ -113,35 +97,6 @@ Description: Toolbox for X-Ray data analysis - Python2 library
.
This is the Python 2 version of the package.
-Package: python-silx-dbg
-Architecture: any
-Section: debug
-Depends: ${misc:Depends},
- ${python:Depends},
- ${shlibs:Depends},
- python-silx (= ${binary:Version}),
- libstdc++6-4.9-dbg,
- python-dbg,
- python-numpy-dbg,
- python-fabio-dbg,
- python-h5py-dbg,
- python-pyopencl-dbg,
- python-mako,
- python-matplotlib-dbg,
- python-dateutil,
- python-opengl,
- python-pyqt5-dbg,
- python-pyqt5.qtsvg-dbg,
- python-pyqt5.qtopengl-dbg,
- python-scipy-dbg,
- python-six,
- python-enum34,
- python-concurrent.futures,
-Description: Toolbox for X-Ray data analysis - python2 debug
- .
- This package contains the extension built for the Python 2 debug
- interpreter.
-
Package: python3-silx
Architecture: any
Section: python
@@ -161,6 +116,7 @@ Depends: ${misc:Depends},
python3-pyqt5.qtsvg,
python3-pyqt5.qtopengl,
python3-scipy,
+ python3-setuptools,
python3-six,
# Recommends:
# Suggests: python3-rfoo
@@ -168,33 +124,6 @@ Description: Toolbox for X-Ray data analysis - Python3
.
This is the Python 3 version of the package.
-Package: python3-silx-dbg
-Architecture: any
-Section: debug
-Depends: ${misc:Depends},
- ${python3:Depends},
- ${shlibs:Depends},
- python3-silx (= ${binary:Version}),
- libstdc++6-4.9-dbg,
- python3-dbg,
- python3-numpy-dbg,
- python3-fabio-dbg,
- python3-h5py-dbg,
- python3-pyopencl-dbg,
- python3-mako,
- python3-matplotlib-dbg,
- python3-dateutil,
- python3-opengl,
- python3-pyqt5-dbg,
- python3-pyqt5.qtsvg-dbg,
- python3-pyqt5.qtopengl-dbg,
- python3-scipy-dbg,
- python3-six,
-Description: Toolbox for X-Ray data analysis - Python3 debug
- .
- This package contains the extension built for the Python 3 debug
- interpreter.
-
Package: python-silx-doc
Architecture: all
Section: doc
diff --git a/package/debian8/rules b/package/debian8/rules
index f72ed66..160147b 100755
--- a/package/debian8/rules
+++ b/package/debian8/rules
@@ -39,10 +39,6 @@ override_dh_install:
rm -rf debian/python-silx/usr/bin
rm -rf debian/python3-silx/usr/bin
- # remove all py/pyc/egg-info files from dbg packages
- find debian/python-silx-dbg/usr -type f \( -not -name "*.so" \) -delete
- find debian/python3-silx-dbg/usr -type f \( -not -name "*.so" \) -delete
-
dh_install
override_dh_auto_test:
diff --git a/package/debian9/control b/package/debian9/control
index fee1b1a..d0de5fe 100644
--- a/package/debian9/control
+++ b/package/debian9/control
@@ -17,14 +17,17 @@ Build-Depends: cython,
python-mako,
python-qtconsole,
python-matplotlib,
+ python-nbsphinx,
python-dateutil,
python-opengl,
python-pyqt5,
python-pyqt5.qtsvg,
python-pyqt5.qtopengl,
python-scipy,
+ python-setuptools,
python-sphinx,
python-sphinxcontrib.programoutput,
+ python-six,
python-enum34,
python-concurrent.futures,
python3-all-dev,
@@ -35,12 +38,15 @@ Build-Depends: cython,
python3-mako,
python3-qtconsole,
python3-matplotlib,
+ python3-nbsphinx,
python3-dateutil,
python3-opengl,
python3-pyqt5,
python3-pyqt5.qtsvg,
python3-pyqt5.qtopengl,
python3-scipy,
+ python3-setuptools,
+ python3-six,
python3-sphinx,
python3-sphinxcontrib.programoutput,
openstack-pkg-tools,
@@ -85,6 +91,7 @@ Depends: ${misc:Depends},
python-pyqt5.qtsvg,
python-pyqt5.qtopengl,
python-scipy,
+ python-setuptools,
python-six,
python-enum34,
python-concurrent.futures,
@@ -115,6 +122,7 @@ Depends: ${misc:Depends},
python3-pyqt5.qtsvg,
python3-pyqt5.qtopengl,
python3-scipy,
+ python3-setuptools,
python3-six,
# Recommends:
# Suggests: python3-rfoo
diff --git a/requirements-dev.txt b/requirements-dev.txt
index cac1f35..62ce91e 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,7 +1,7 @@
# List of silx development dependencies
# Those ARE NOT required for installation, at runtime or to build from source (except for the doc)
-numpy >= 1.8
+-r requirements.txt
setuptools # Advanced packaging tools
wheel # To build wheels
Cython >= 0.21.1 # To regenerate .c/.cpp files from .pyx
@@ -10,6 +10,7 @@ lxml # For test coverage in run_test.py
coverage # For test coverage in run_test.py
pillow # For loading images in documentation generation
nbsphinx # For converting ipynb in documentation
+pandoc # For documentation Qt snapshot updates
# Use dev version of PyInstaller to keep hooks up-to-date
https://github.com/pyinstaller/pyinstaller/archive/develop.zip; sys_platform == "win32"
diff --git a/requirements.txt b/requirements.txt
index 90dc020..cdffb47 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,17 +5,24 @@
--find-links http://www.silx.org/pub/wheelhouse/
--only-binary numpy,h5py,scipy,PyQt4,PyQt5
-numpy >= 1.8
-fabio >= 0.7
+# Required dependencies (from setup.py install_requires)
+numpy >= 1.12
+setuptools
h5py
-scipy # For silx.math.fit demo, silx.image.sift demo, silx.image.sift.test
-pyopencl; platform_machine in "i386, x86_64" # For silx.opencl
+fabio >= 0.7
+six
+enum34; python_version == '2.7'
+futures; python_version == '2.7'
+
+# Extra dependencies (from setup.py extra_requires 'full' target)
+pyopencl; platform_machine in "i386, x86_64, AMD64" # For silx.opencl
Mako # For pyopencl reduction
qtconsole # For silx.gui.console
matplotlib >= 1.2.0 # For silx.gui.plot
PyOpenGL # For silx.gui.plot3d
-Pillow # For silx.opencl.image.test
python-dateutil # For silx.gui.plot
+scipy # For silx.math.fit demo, silx.image.sift demo, silx.image.sift.test
+Pillow # For silx.opencl.image.test
# PyQt5, PySide2 or PyQt4 # For silx.gui
# Try to install a Qt binding from a wheel
diff --git a/run_tests.py b/run_tests.py
index bea6625..6007344 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -115,7 +115,6 @@ except Exception as error:
else:
logger.info("Numpy %s", numpy.version.version)
-
try:
import h5py
except Exception as error:
@@ -314,10 +313,10 @@ def import_project_module(project_name, project_dir):
if "--installed" in sys.argv:
try:
module = importer(project_name)
- except ImportError:
- raise ImportError(
- "%s not installed: Cannot run tests on installed version" %
- PROJECT_NAME)
+ except Exception:
+ logger.error("Cannot run tests on installed version: %s not installed or raising error.",
+ project_name)
+ raise
else: # Use built source
build_dir = build_project(project_name, project_dir)
if build_dir is None:
diff --git a/setup.py b/setup.py
index 9f6ae13..46d0bdf 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@
# coding: utf8
# /*##########################################################################
#
-# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -25,7 +25,7 @@
# ###########################################################################*/
__authors__ = ["Jérôme Kieffer", "Thomas Vincent"]
-__date__ = "23/04/2018"
+__date__ = "12/02/2019"
__license__ = "MIT"
@@ -40,6 +40,7 @@ import glob
# The silx.io module seems to be loaded instead.
import io
+
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("silx.setup")
@@ -84,7 +85,10 @@ export LC_ALL=en_US.utf-8
def get_version():
"""Returns current version number from version.py file"""
+ dirname = os.path.dirname(os.path.abspath(__file__))
+ sys.path.insert(0, dirname)
import version
+ sys.path = sys.path[1:]
return version.strictversion
@@ -111,7 +115,6 @@ classifiers = ["Development Status :: 4 - Beta",
"Operating System :: POSIX",
"Programming Language :: Cython",
"Programming Language :: Python :: 2.7",
- "Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
@@ -346,8 +349,20 @@ if sphinx is not None:
self.mkpath(self.builder_target_dir)
BuildDoc.run(self)
sys.path.pop(0)
+
+ class BuildDocAndGenerateScreenshotCommand(BuildDocCommand):
+ def run(self):
+ old = os.environ.get('DIRECTIVE_SNAPSHOT_QT')
+ os.environ['DIRECTIVE_SNAPSHOT_QT'] = 'True'
+ BuildDocCommand.run(self)
+ if old is not None:
+ os.environ['DIRECTIVE_SNAPSHOT_QT'] = old
+ else:
+ del os.environ['DIRECTIVE_SNAPSHOT_QT']
+
else:
BuildDocCommand = SphinxExpectedCommand
+ BuildDocAndGenerateScreenshotCommand = SphinxExpectedCommand
# ################### #
@@ -475,7 +490,7 @@ class Build(_build):
# By default Xcode5 & XCode6 do not support OpenMP, Xcode4 is OK.
osx = tuple([int(i) for i in platform.mac_ver()[0].split(".")])
if osx >= (10, 8):
- logger.warning("OpenMP support ignored. Your platform do not support it")
+ logger.warning("OpenMP support ignored. Your platform does not support it.")
use_openmp = False
# Remove attributes used by distutils parsing
@@ -542,7 +557,7 @@ class BuildExt(build_ext):
LINK_ARGS_CONVERTER = {'-fopenmp': ''}
- description = 'Build silx extensions'
+ description = 'Build extensions'
def finalize_options(self):
build_ext.finalize_options(self)
@@ -589,7 +604,8 @@ class BuildExt(build_ext):
from Cython.Build import cythonize
patched_exts = cythonize(
[ext],
- compiler_directives={'embedsignature': True},
+ compiler_directives={'embedsignature': True,
+ 'language_level': 3},
force=self.force_cython
)
ext.sources = patched_exts[0].sources
@@ -603,10 +619,15 @@ class BuildExt(build_ext):
# Convert flags from gcc to MSVC if required
if self.compiler.compiler_type == 'msvc':
- ext.extra_compile_args = [self.COMPILE_ARGS_CONVERTER.get(f, f)
- for f in ext.extra_compile_args]
- ext.extra_link_args = [self.LINK_ARGS_CONVERTER.get(f, f)
- for f in ext.extra_link_args]
+ extra_compile_args = [self.COMPILE_ARGS_CONVERTER.get(f, f)
+ for f in ext.extra_compile_args]
+ # Avoid empty arg
+ ext.extra_compile_args = [arg for arg in extra_compile_args if arg]
+
+ extra_link_args = [self.LINK_ARGS_CONVERTER.get(f, f)
+ for f in ext.extra_link_args]
+ # Avoid empty arg
+ ext.extra_link_args = [arg for arg in extra_link_args if arg]
elif self.compiler.compiler_type == 'unix':
# Avoids runtime symbol collision for manylinux1 platform
@@ -614,8 +635,17 @@ class BuildExt(build_ext):
extern = 'extern "C" ' if ext.language == 'c++' else ''
return_type = 'void' if sys.version_info[0] <= 2 else 'PyObject*'
- # ext.extra_compile_args.append(
- # '''-fvisibility=hidden -D'PyMODINIT_FUNC=%s__attribute__((visibility("default"))) %s ' ''' % (extern, return_type))
+ ext.extra_compile_args.append('-fvisibility=hidden')
+
+ import numpy
+ numpy_version = [int(i) for i in numpy.version.short_version.split(".", 2)[:2]]
+ if numpy_version < [1, 16]:
+ ext.extra_compile_args.append(
+ '''-D'PyMODINIT_FUNC=%s__attribute__((visibility("default"))) %s ' ''' % (extern, return_type))
+ else:
+ ext.define_macros.append(
+ ('PyMODINIT_FUNC',
+ '%s__attribute__((visibility("default"))) %s ' % (extern, return_type)))
def is_debug_interpreter(self):
"""
@@ -678,6 +708,7 @@ class BuildExt(build_ext):
self.patch_extension(ext)
build_ext.build_extensions(self)
+
################################################################################
# Clean command
################################################################################
@@ -756,7 +787,7 @@ class SourceDistWithCython(sdist):
without suppport of OpenMP.
"""
- description = "Create a source distribution including cythonozed files (tarball, zip file, etc.)"
+ description = "Create a source distribution including cythonized files (tarball, zip file, etc.)"
def finalize_options(self):
sdist.finalize_options(self)
@@ -770,7 +801,8 @@ class SourceDistWithCython(sdist):
from Cython.Build import cythonize
cythonize(
self.extensions,
- compiler_directives={'embedsignature': True},
+ compiler_directives={'embedsignature': True,
+ 'language_level': 3},
force=True
)
@@ -860,9 +892,20 @@ def get_project_configuration(dry_run):
"setuptools",
# for io support
"h5py",
- "fabio>=0.7"]
+ "fabio>=0.7",
+ # Python 2/3 compatibility
+ "six",
+ ]
+
+ # Add Python 2.7 backports
+ # Equivalent to but supported by old setuptools:
+ # "enum34; python_version == '2.7'",
+ # "futures; python_version == '2.7'",
+ if sys.version_info[0] == 2:
+ install_requires.append("enum34")
+ install_requires.append("futures")
- setup_requires = ["setuptools", "numpy"]
+ setup_requires = ["setuptools", "numpy>=1.12"]
# extras requirements: target 'full' to install all dependencies at once
full_requires = [
@@ -883,6 +926,12 @@ def get_project_configuration(dry_run):
'full': full_requires,
}
+ # Here for packaging purpose only
+ # Setting the SILX_FULL_INSTALL_REQUIRES environment variable
+ # put all dependencies as install_requires
+ if os.environ.get('SILX_FULL_INSTALL_REQUIRES') is not None:
+ install_requires += full_requires
+
package_data = {
# Resources files for silx
'silx.resources': [
@@ -910,6 +959,7 @@ def get_project_configuration(dry_run):
build=Build,
build_py=build_py,
test=PyTest,
+ build_screenshots=BuildDocAndGenerateScreenshotCommand,
build_doc=BuildDocCommand,
test_doc=TestDocCommand,
build_ext=BuildExt,
diff --git a/silx.egg-info/PKG-INFO b/silx.egg-info/PKG-INFO
index 52f365a..1f1f36b 100644
--- a/silx.egg-info/PKG-INFO
+++ b/silx.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: silx
-Version: 0.9.0
+Version: 0.11.0
Summary: Software library for X-ray data analysis
Home-page: http://www.silx.org/
Author: data analysis unit
@@ -24,7 +24,8 @@ Description:
images file formats.
* OpenCL-based data processing: image alignment (SIFT),
image processing (median filter, histogram),
- filtered backprojection for tomography
+ filtered backprojection for tomography,
+ convolution
* Data reduction: histogramming, fitting, median filter
* A set of Qt widgets, including:
@@ -55,13 +56,13 @@ Description:
Or using Anaconda on Linux and MacOS:
- .. code-block:: bash
-
+ .. code-block:: bash
+
conda install silx -c conda-forge
Unofficial packages for different distributions are available:
- - Unofficial Debian8 packages are available at http://www.silx.org/pub/debian/
+ - Unofficial Debian9 packages are available at http://www.silx.org/pub/debian/
- CentOS 7 rpm packages are provided by Max IV at: http://pubrepo.maxiv.lu.se/rpm/el7/x86_64/
- Fedora 23 rpm packages are provided by Max IV at http://pubrepo.maxiv.lu.se/rpm/fc23/x86_64/
- Arch Linux (AUR) packages are also available: https://aur.archlinux.org/packages/python-silx
@@ -129,7 +130,6 @@ Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Cython
Classifier: Programming Language :: Python :: 2.7
-Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
diff --git a/silx.egg-info/SOURCES.txt b/silx.egg-info/SOURCES.txt
index 2a926d4..024a121 100644
--- a/silx.egg-info/SOURCES.txt
+++ b/silx.egg-info/SOURCES.txt
@@ -62,6 +62,7 @@ doc/source/description/img/sift_frame_ROI.png
doc/source/description/img/sift_match1.png
doc/source/description/img/sift_match2.png
doc/source/description/img/sift_orientation.png
+doc/source/ext/snapshotqt_directive.py
doc/source/ext/sphinxext-archive.py
doc/source/img/silx.ico
doc/source/img/silx_large.png
@@ -137,6 +138,7 @@ doc/source/modules/gui/plot/scatterview.rst
doc/source/modules/gui/plot/stackview.rst
doc/source/modules/gui/plot/statswidget.rst
doc/source/modules/gui/plot/tools.rst
+doc/source/modules/gui/plot/utils.rst
doc/source/modules/gui/plot/actions/control.rst
doc/source/modules/gui/plot/actions/examples.rst
doc/source/modules/gui/plot/actions/fit.rst
@@ -148,6 +150,8 @@ doc/source/modules/gui/plot/actions/img/fftAction0.png
doc/source/modules/gui/plot/actions/img/fftAction1.png
doc/source/modules/gui/plot/actions/img/shiftAction0.png
doc/source/modules/gui/plot/actions/img/shiftAction3.png
+doc/source/modules/gui/plot/img/BasicGridStatsWidget.png
+doc/source/modules/gui/plot/img/BasicStatsWidget.png
doc/source/modules/gui/plot/img/CompareImages.png
doc/source/modules/gui/plot/img/ComplexImageView.png
doc/source/modules/gui/plot/img/CurveLegendsWidget.png
@@ -252,9 +256,13 @@ doc/source/modules/math/fit/index.rst
doc/source/modules/math/fit/leastsq.rst
doc/source/modules/math/fit/peaksearch.rst
doc/source/modules/opencl/codec_cbf.rst
+doc/source/modules/opencl/convolution.rst
doc/source/modules/opencl/fbp.rst
doc/source/modules/opencl/index.rst
doc/source/modules/opencl/medfilt.rst
+doc/source/modules/opencl/processing.rst
+doc/source/modules/opencl/sinofilter.rst
+doc/source/modules/opencl/statistics.rst
doc/source/modules/opencl/sift/align.rst
doc/source/modules/opencl/sift/index.rst
doc/source/modules/opencl/sift/match.rst
@@ -279,6 +287,7 @@ doc/source/sample_code/img/imageview.png
doc/source/sample_code/img/periodicTable.png
doc/source/sample_code/img/plot3dContextMenu.png
doc/source/sample_code/img/plot3dSceneWindow.png
+doc/source/sample_code/img/plot3dUpdateScatterFromThread.png
doc/source/sample_code/img/plotClearAction.png
doc/source/sample_code/img/plotContextMenu.png
doc/source/sample_code/img/plotInteractiveImageROI.png
@@ -299,6 +308,7 @@ examples/colormapDialog.py
examples/compareImages.py
examples/customDataView.py
examples/customHdf5TreeModel.py
+examples/dropZones.py
examples/fft.png
examples/fftPlotAction.py
examples/fileDialog.py
@@ -309,6 +319,7 @@ examples/imageview.py
examples/periodicTable.py
examples/plot3dContextMenu.py
examples/plot3dSceneWindow.py
+examples/plot3dUpdateScatterFromThread.py
examples/plotClearAction.py
examples/plotContextMenu.py
examples/plotCurveLegendWidget.py
@@ -324,6 +335,7 @@ examples/scatterMask.py
examples/shiftPlotAction.py
examples/simplewidget.py
examples/stackView.py
+examples/syncPlotLocation.py
examples/syncaxis.py
examples/viewer3DVolume.py
examples/writetoh5.py
@@ -419,6 +431,7 @@ silx/gui/data/NXdataWidgets.py
silx/gui/data/NumpyAxesSelector.py
silx/gui/data/RecordTableView.py
silx/gui/data/TextFormatter.py
+silx/gui/data/_VolumeWindow.py
silx/gui/data/__init__.py
silx/gui/data/setup.py
silx/gui/data/test/__init__.py
@@ -498,6 +511,7 @@ silx/gui/plot/_BaseMaskToolsWidget.py
silx/gui/plot/__init__.py
silx/gui/plot/setup.py
silx/gui/plot/_utils/__init__.py
+silx/gui/plot/_utils/delaunay.py
silx/gui/plot/_utils/dtime_ticklayout.py
silx/gui/plot/_utils/panzoom.py
silx/gui/plot/_utils/setup.py
@@ -521,6 +535,7 @@ silx/gui/plot/backends/__init__.py
silx/gui/plot/backends/glutils/GLPlotCurve.py
silx/gui/plot/backends/glutils/GLPlotFrame.py
silx/gui/plot/backends/glutils/GLPlotImage.py
+silx/gui/plot/backends/glutils/GLPlotTriangles.py
silx/gui/plot/backends/glutils/GLSupport.py
silx/gui/plot/backends/glutils/GLText.py
silx/gui/plot/backends/glutils/GLTexture.py
@@ -630,7 +645,10 @@ silx/gui/plot3d/scene/test/test_utils.py
silx/gui/plot3d/test/__init__.py
silx/gui/plot3d/test/testGL.py
silx/gui/plot3d/test/testScalarFieldView.py
+silx/gui/plot3d/test/testSceneWidget.py
silx/gui/plot3d/test/testSceneWidgetPicking.py
+silx/gui/plot3d/test/testSceneWindow.py
+silx/gui/plot3d/test/testStatsWidget.py
silx/gui/plot3d/tools/GroupPropertiesWidget.py
silx/gui/plot3d/tools/PositionInfoWidget.py
silx/gui/plot3d/tools/ViewpointTools.py
@@ -656,6 +674,7 @@ silx/gui/test/utils.py
silx/gui/utils/__init__.py
silx/gui/utils/concurrent.py
silx/gui/utils/image.py
+silx/gui/utils/projecturl.py
silx/gui/utils/testutils.py
silx/gui/utils/test/__init__.py
silx/gui/utils/test/test_async.py
@@ -672,6 +691,7 @@ silx/gui/widgets/PrintPreview.py
silx/gui/widgets/RangeSlider.py
silx/gui/widgets/TableWidget.py
silx/gui/widgets/ThreadPoolPushButton.py
+silx/gui/widgets/UrlSelectionTable.py
silx/gui/widgets/WaitingPushButton.py
silx/gui/widgets/__init__.py
silx/gui/widgets/setup.py
@@ -778,6 +798,16 @@ silx/math/marchingcubes.pyx
silx/math/math_compatibility.pxd
silx/math/mc.pxd
silx/math/setup.py
+silx/math/fft/__init__.py
+silx/math/fft/basefft.py
+silx/math/fft/clfft.py
+silx/math/fft/cufft.py
+silx/math/fft/fft.py
+silx/math/fft/fftw.py
+silx/math/fft/npfft.py
+silx/math/fft/setup.py
+silx/math/fft/test/__init__.py
+silx/math/fft/test/test_fft.py
silx/math/fit/__init__.py
silx/math/fit/bgtheories.py
silx/math/fit/filters.c
@@ -842,6 +872,7 @@ silx/math/test/test_marchingcubes.py
silx/opencl/__init__.py
silx/opencl/backprojection.py
silx/opencl/common.py
+silx/opencl/convolution.py
silx/opencl/image.py
silx/opencl/linalg.py
silx/opencl/medfilt.py
@@ -849,6 +880,9 @@ silx/opencl/processing.py
silx/opencl/projection.py
silx/opencl/reconstruction.py
silx/opencl/setup.py
+silx/opencl/sinofilter.py
+silx/opencl/sparse.py
+silx/opencl/statistics.py
silx/opencl/utils.py
silx/opencl/codec/__init__.py
silx/opencl/codec/byte_offset.py
@@ -880,10 +914,14 @@ silx/opencl/test/__init__.py
silx/opencl/test/test_addition.py
silx/opencl/test/test_array_utils.py
silx/opencl/test/test_backprojection.py
+silx/opencl/test/test_convolution.py
silx/opencl/test/test_image.py
+silx/opencl/test/test_kahan.py
silx/opencl/test/test_linalg.py
silx/opencl/test/test_medfilt.py
silx/opencl/test/test_projection.py
+silx/opencl/test/test_sparse.py
+silx/opencl/test/test_stats.py
silx/resources/__init__.py
silx/resources/gui/colormaps/inferno.npy
silx/resources/gui/colormaps/magma.npy
@@ -945,6 +983,8 @@ silx/resources/gui/icons/compare-align-stretch.png
silx/resources/gui/icons/compare-align-stretch.svg
silx/resources/gui/icons/compare-keypoints.png
silx/resources/gui/icons/compare-keypoints.svg
+silx/resources/gui/icons/compare-mode-a-minus-b.png
+silx/resources/gui/icons/compare-mode-a-minus-b.svg
silx/resources/gui/icons/compare-mode-a.png
silx/resources/gui/icons/compare-mode-a.svg
silx/resources/gui/icons/compare-mode-b.png
@@ -991,6 +1031,8 @@ silx/resources/gui/icons/draw-rubber.png
silx/resources/gui/icons/draw-rubber.svg
silx/resources/gui/icons/edit-copy.png
silx/resources/gui/icons/edit-copy.svg
+silx/resources/gui/icons/eye.png
+silx/resources/gui/icons/eye.svg
silx/resources/gui/icons/first.png
silx/resources/gui/icons/first.svg
silx/resources/gui/icons/folder.png
@@ -1121,6 +1163,8 @@ silx/resources/gui/icons/plot-ylog.png
silx/resources/gui/icons/plot-ylog.svg
silx/resources/gui/icons/plot-yup.png
silx/resources/gui/icons/plot-yup.svg
+silx/resources/gui/icons/pointing-hand.png
+silx/resources/gui/icons/pointing-hand.svg
silx/resources/gui/icons/previous.png
silx/resources/gui/icons/previous.svg
silx/resources/gui/icons/process-working.mng
@@ -1250,10 +1294,15 @@ silx/resources/opencl/array_utils.cl
silx/resources/opencl/backproj.cl
silx/resources/opencl/backproj_helper.cl
silx/resources/opencl/bitonic.cl
+silx/resources/opencl/convolution.cl
+silx/resources/opencl/convolution_textures.cl
+silx/resources/opencl/kahan.cl
silx/resources/opencl/linalg.cl
silx/resources/opencl/medfilt.cl
silx/resources/opencl/preprocess.cl
silx/resources/opencl/proj.cl
+silx/resources/opencl/sparse.cl
+silx/resources/opencl/statistics.cl
silx/resources/opencl/codec/byte_offset.cl
silx/resources/opencl/image/cast.cl
silx/resources/opencl/image/histogram.cl
@@ -1280,28 +1329,17 @@ silx/resources/opencl/sift/transform.cl
silx/sx/__init__.py
silx/sx/_plot.py
silx/sx/_plot3d.py
-silx/sx/test/__init__.py
-silx/sx/test/test_sx.py
silx/test/__init__.py
silx/test/test_resources.py
+silx/test/test_sx.py
silx/test/test_version.py
silx/test/utils.py
silx/third_party/EdfFile.py
silx/third_party/TiffIO.py
silx/third_party/__init__.py
-silx/third_party/concurrent_futures.py
-silx/third_party/enum.py
-silx/third_party/modest_image.py
silx/third_party/scipy_spatial.py
silx/third_party/setup.py
-silx/third_party/six.py
silx/third_party/_local/__init__.py
-silx/third_party/_local/enum.py
-silx/third_party/_local/six.py
-silx/third_party/_local/concurrent_futures/__init__.py
-silx/third_party/_local/concurrent_futures/_base.py
-silx/third_party/_local/concurrent_futures/process.py
-silx/third_party/_local/concurrent_futures/thread.py
silx/third_party/_local/scipy_spatial/__init__.py
silx/third_party/_local/scipy_spatial/qhull.c
silx/third_party/_local/scipy_spatial/qhull.pxd
@@ -1338,12 +1376,15 @@ silx/third_party/_local/scipy_spatial/qhull/src/user_r.h
silx/third_party/_local/scipy_spatial/qhull/src/usermem_r.c
silx/third_party/_local/scipy_spatial/qhull/src/userprintf_r.c
silx/third_party/_local/scipy_spatial/qhull/src/userprintf_rbox_r.c
+silx/utils/ExternalResources.py
silx/utils/__init__.py
silx/utils/_have_openmp.pxi
silx/utils/array_like.py
silx/utils/debug.py
silx/utils/deprecation.py
+silx/utils/enum.py
silx/utils/exceptions.py
+silx/utils/files.py
silx/utils/html.py
silx/utils/launcher.py
silx/utils/number.py
@@ -1357,6 +1398,8 @@ silx/utils/test/__init__.py
silx/utils/test/test_array_like.py
silx/utils/test/test_debug.py
silx/utils/test/test_deprecation.py
+silx/utils/test/test_enum.py
+silx/utils/test/test_external_resources.py
silx/utils/test/test_html.py
silx/utils/test/test_launcher.py
silx/utils/test/test_launcher_command.py
diff --git a/silx.egg-info/requires.txt b/silx.egg-info/requires.txt
index e7cfb25..79c4d9c 100644
--- a/silx.egg-info/requires.txt
+++ b/silx.egg-info/requires.txt
@@ -1,7 +1,8 @@
-numpy>=1.15.3
+numpy>=1.12.0
setuptools
h5py
fabio>=0.7
+six
[full]
pyopencl
diff --git a/silx/_config.py b/silx/_config.py
index 02bbf4e..fb0e409 100644
--- a/silx/_config.py
+++ b/silx/_config.py
@@ -2,7 +2,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -28,7 +28,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "26/04/2018"
+__date__ = "09/11/2018"
class Config(object):
@@ -38,7 +38,7 @@ class Config(object):
.. versionadded:: 0.8
"""
- DEFAULT_PLOT_BACKEND = "matplotlib"
+ DEFAULT_PLOT_BACKEND = "matplotlib", "opengl"
"""Default plot backend.
It will be used as default backend for all the next created PlotWidget.
@@ -51,13 +51,15 @@ class Config(object):
- A :class:`silx.gui.plot.backend.BackendBase.BackendBase` class
- A callable returning backend class or binding name
+ If multiple backends are provided, the first available one is used.
+
.. versionadded:: 0.8
"""
DEFAULT_COLORMAP_NAME = 'gray'
"""Default LUT for the plot widgets.
- The available list of names are availaible in the module
+ The available list of names are available in the module
:module:`silx.gui.colors`.
.. versionadded:: 0.8
@@ -109,4 +111,38 @@ class Config(object):
.. versionadded:: 0.9
"""
-
+
+ DEFAULT_PLOT_CURVE_SYMBOL_MODE = False
+ """Whether to display curves with markers or not by default in PlotWidget.
+
+ It will have an influence on PlotWidget curve items.
+
+ .. versionadded:: 0.10
+ """
+
+ DEFAULT_PLOT_SYMBOL = 'o'
+ """Default marker of the item.
+
+ It will have an influence on PlotWidget items
+
+ Supported symbols:
+
+ - 'o', 'Circle'
+ - 'd', 'Diamond'
+ - 's', 'Square'
+ - '+', 'Plus'
+ - 'x', 'Cross'
+ - '.', 'Point'
+ - ',', 'Pixel'
+ - '', 'None'
+
+ .. versionadded:: 0.10
+ """
+
+ DEFAULT_PLOT_SYMBOL_SIZE = 6.0
+ """Default marker size of the item.
+
+ It will have an influence on PlotWidget items
+
+ .. versionadded:: 0.10
+ """
diff --git a/silx/app/convert.py b/silx/app/convert.py
index a8c2783..7e601ce 100644
--- a/silx/app/convert.py
+++ b/silx/app/convert.py
@@ -23,29 +23,23 @@
# ############################################################################*/
"""Convert silx supported data files into HDF5 files"""
+__authors__ = ["P. Knobel"]
+__license__ = "MIT"
+__date__ = "05/02/2019"
+
import ast
import os
import argparse
from glob import glob
import logging
-import numpy
import re
import time
+import numpy
+import six
import silx.io
from silx.io.specfile import is_specfile
-from silx.third_party import six
-
-try:
- from silx.io import fabioh5
-except ImportError:
- fabioh5 = None
-
-
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__date__ = "12/09/2017"
-
+from silx.io import fabioh5
_logger = logging.getLogger(__name__)
"""Module logger"""
@@ -306,20 +300,14 @@ def main(argv):
_logger.debug("Backtrace", exc_info=True)
hdf5plugin = None
+ import h5py
+
try:
- import h5py
from silx.io.convert import write_to_h5
except ImportError:
_logger.debug("Backtrace", exc_info=True)
- h5py = None
write_to_h5 = None
- if h5py is None:
- message = "Module 'h5py' is not installed but is mandatory."\
- + " You can install it using \"pip install h5py\"."
- _logger.error(message)
- return -1
-
if hdf5plugin is None:
message = "Module 'hdf5plugin' is not installed. It supports additional hdf5"\
+ " compressions. You can install it using \"pip install hdf5plugin\"."
@@ -455,7 +443,11 @@ def main(argv):
create_dataset_args["chunks"] = chunks
if options.compression is not None:
- create_dataset_args["compression"] = options.compression
+ try:
+ compression = int(options.compression)
+ except ValueError:
+ compression = options.compression
+ create_dataset_args["compression"] = compression
if options.compression_opts is not None:
create_dataset_args["compression_opts"] = options.compression_opts
@@ -470,18 +462,6 @@ def main(argv):
not contains_specfile(options.input_files) and
not options.add_root_group) or options.file_pattern is not None:
# File series -> stack of images
- if fabioh5 is None:
- # return a helpful error message if fabio is missing
- try:
- import fabio
- except ImportError:
- _logger.error("The fabio library is required to convert"
- " edf files. Please install it with 'pip "
- "install fabio` and try again.")
- else:
- # unexpected problem in silx.io.fabioh5
- raise
- return -1
input_group = fabioh5.File(file_series=options.input_files)
if hdf5_path != "/":
# we want to append only data and headers to an existing file
diff --git a/silx/app/test/test_convert.py b/silx/app/test/test_convert.py
index 97be3fd..bb1ae99 100644
--- a/silx/app/test/test_convert.py
+++ b/silx/app/test/test_convert.py
@@ -35,11 +35,7 @@ import tempfile
import unittest
import io
import gc
-
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
import silx
from .. import convert
@@ -103,14 +99,6 @@ class TestConvertCommand(unittest.TestCase):
result = e.args[0]
self.assertEqual(result, 0)
- @testutils.test_logging(convert._logger.name, error=1)
- def testH5pyNotInstalled(self):
- with testutils.EnsureImportError("h5py"):
- result = convert.main(["convert", "foo.spec", "bar.edf"])
- # we explicitly return -1 if h5py is not imported
- self.assertNotEqual(result, 0)
-
- @unittest.skipIf(h5py is None, "h5py is required to test convert")
def testWrongOption(self):
# presence of a wrong option must cause a SystemExit or a return
# with a non-zero status
@@ -120,14 +108,12 @@ class TestConvertCommand(unittest.TestCase):
result = e.args[0]
self.assertNotEqual(result, 0)
- @unittest.skipIf(h5py is None, "h5py is required to test convert")
@testutils.test_logging(convert._logger.name, error=3)
# one error log per missing file + one "Aborted" error log
def testWrongFiles(self):
result = convert.main(["convert", "foo.spec", "bar.edf"])
self.assertNotEqual(result, 0)
- @unittest.skipIf(h5py is None, "h5py is required to test convert")
def testFile(self):
# create a writable temp directory
tempdir = tempfile.mkdtemp()
diff --git a/silx/app/view/About.py b/silx/app/view/About.py
index 4b804f2..a2b430f 100644
--- a/silx/app/view/About.py
+++ b/silx/app/view/About.py
@@ -1,6 +1,6 @@
# coding: utf-8
# /*##########################################################################
-# Copyright (C) 2016-2017 European Synchrotron Radiation Facility
+# Copyright (C) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -27,6 +27,7 @@ __authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "05/07/2018"
+import os
import sys
from silx.gui import qt
@@ -208,7 +209,7 @@ class About(qt.QDialog):
pass
# Access to the logo in SVG or PNG
- logo = icons.getQFile("../logo/silx")
+ logo = icons.getQFile("silx:" + os.path.join("gui", "logo", "silx"))
info = dict(
application_name=self.__applicationName,
diff --git a/silx/app/view/Viewer.py b/silx/app/view/Viewer.py
index 88ff989..d543352 100644
--- a/silx/app/view/Viewer.py
+++ b/silx/app/view/Viewer.py
@@ -1,6 +1,6 @@
# coding: utf-8
# /*##########################################################################
-# Copyright (C) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (C) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -25,7 +25,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "08/10/2018"
+__date__ = "15/01/2019"
import os
@@ -41,6 +41,7 @@ from .ApplicationContext import ApplicationContext
from .CustomNxdataWidget import CustomNxdataWidget
from .CustomNxdataWidget import CustomNxDataToolBar
from . import utils
+from silx.gui.utils import projecturl
from .DataPanel import DataPanel
@@ -61,6 +62,9 @@ class Viewer(qt.QMainWindow):
qt.QMainWindow.__init__(self, parent)
self.setWindowTitle("Silx viewer")
+ silxIcon = icons.getQIcon("silx")
+ self.setWindowIcon(silxIcon)
+
self.__context = ApplicationContext(self, settings)
self.__context.restoreLibrarySettings()
@@ -74,6 +78,7 @@ class Viewer(qt.QMainWindow):
rightPanel.setOrientation(qt.Qt.Vertical)
self.__splitter2 = rightPanel
+ self.__displayIt = None
self.__treeWindow = self.__createTreeWindow(self.__treeview)
# Custom the model to be able to manage the life cycle of the files
@@ -150,11 +155,26 @@ class Viewer(qt.QMainWindow):
action.setText("Refresh")
action.setToolTip("Refresh all selected items")
action.triggered.connect(self.__refreshSelected)
- action.setShortcut(qt.QKeySequence(qt.Qt.ControlModifier + qt.Qt.Key_Plus))
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_F5))
toolbar.addAction(action)
treeView.addAction(action)
self.__refreshAction = action
+ # Another shortcut for refresh
+ action = qt.QAction(toolbar)
+ action.setShortcut(qt.QKeySequence(qt.Qt.ControlModifier + qt.Qt.Key_R))
+ treeView.addAction(action)
+ action.triggered.connect(self.__refreshSelected)
+
+ action = qt.QAction(toolbar)
+ # action.setIcon(icons.getQIcon("view-refresh"))
+ action.setText("Close")
+ action.setToolTip("Close selected item")
+ action.triggered.connect(self.__removeSelected)
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_Delete))
+ treeView.addAction(action)
+ self.__closeAction = action
+
toolbar.addSeparator()
action = qt.QAction(toolbar)
@@ -185,6 +205,37 @@ class Viewer(qt.QMainWindow):
layout.addWidget(treeView)
return widget
+ def __removeSelected(self):
+ """Close selected items"""
+ qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
+
+ selection = self.__treeview.selectionModel()
+ indexes = selection.selectedIndexes()
+ selectedItems = []
+ model = self.__treeview.model()
+ h5files = set([])
+ while len(indexes) > 0:
+ index = indexes.pop(0)
+ if index.column() != 0:
+ continue
+ h5 = model.data(index, role=silx.gui.hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE)
+ rootIndex = index
+ # Reach the root of the tree
+ while rootIndex.parent().isValid():
+ rootIndex = rootIndex.parent()
+ rootRow = rootIndex.row()
+ relativePath = self.__getRelativePath(model, rootIndex, index)
+ selectedItems.append((rootRow, relativePath))
+ h5files.add(h5.file)
+
+ if len(h5files) != 0:
+ model = self.__treeview.findHdf5TreeModel()
+ for h5 in h5files:
+ row = model.h5pyObjectRow(h5)
+ model.removeH5pyObject(h5)
+
+ qt.QApplication.restoreOverrideCursor()
+
def __refreshSelected(self):
"""Refresh all selected items
"""
@@ -387,6 +438,9 @@ class Viewer(qt.QMainWindow):
def __h5FileLoaded(self, loadedH5):
self.__context.pushRecentFile(loadedH5.file.filename)
+ if loadedH5.file.filename == self.__displayIt:
+ self.__displayIt = None
+ self.displayData(loadedH5)
def __h5FileRemoved(self, removedH5):
self.__dataPanel.removeDatasetsFrom(removedH5)
@@ -402,10 +456,11 @@ class Viewer(qt.QMainWindow):
self.__context.saveSettings()
# Clean up as much as possible Python objects
- model = self.__customNxdata.model()
- model.clear()
- model = self.__treeview.findHdf5TreeModel()
- model.clear()
+ self.displayData(None)
+ customModel = self.__customNxdata.model()
+ customModel.clear()
+ hdf5Model = self.__treeview.findHdf5TreeModel()
+ hdf5Model.clear()
def saveSettings(self, settings):
"""Save the window settings to this settings object
@@ -502,6 +557,11 @@ class Viewer(qt.QMainWindow):
action.triggered.connect(self.about)
self._aboutAction = action
+ action = qt.QAction("&Documentation", self)
+ action.setStatusTip("Show the Silx library's documentation")
+ action.triggered.connect(self.showDocumentation)
+ self._documentationAction = action
+
# Plot backend
action = qt.QAction("Plot rendering backend", self)
@@ -563,7 +623,7 @@ class Viewer(qt.QMainWindow):
action = qt.QAction("Show custom NXdata selector", self)
action.setStatusTip("Show a widget which allow to create plot by selecting data and axes")
action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_F5))
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_F6))
action.toggled.connect(self.__toggleCustomNxdataWindow)
self._displayCustomNxdataWindow = action
@@ -662,6 +722,7 @@ class Viewer(qt.QMainWindow):
helpMenu = self.menuBar().addMenu("&Help")
helpMenu.addAction(self._aboutAction)
+ helpMenu.addAction(self._documentationAction)
def open(self):
dialog = self.createFileDialog()
@@ -691,18 +752,11 @@ class Viewer(qt.QMainWindow):
for description, ext in silx.io.supported_extensions().items():
extensions[description] = " ".join(sorted(list(ext)))
- try:
- # NOTE: hdf5plugin have to be loaded before
- import fabio
- except Exception:
- _logger.debug("Backtrace while loading fabio", exc_info=True)
- fabio = None
-
- if fabio is not None:
- extensions["NeXus layout from EDF files"] = "*.edf"
- extensions["NeXus layout from TIFF image files"] = "*.tif *.tiff"
- extensions["NeXus layout from CBF files"] = "*.cbf"
- extensions["NeXus layout from MarCCD image files"] = "*.mccd"
+ # Add extensions supported by fabio
+ extensions["NeXus layout from EDF files"] = "*.edf"
+ extensions["NeXus layout from TIFF image files"] = "*.tif *.tiff"
+ extensions["NeXus layout from CBF files"] = "*.cbf"
+ extensions["NeXus layout from MarCCD image files"] = "*.mccd"
all_supported_extensions = set()
for name, exts in extensions.items():
@@ -724,6 +778,11 @@ class Viewer(qt.QMainWindow):
from .About import About
About.about(self, "Silx viewer")
+ def showDocumentation(self):
+ subpath = "index.html"
+ url = projecturl.getDocumentationUrl(subpath)
+ qt.QDesktopServices.openUrl(qt.QUrl(url))
+
def __forcePlotImageDownward(self):
silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION = "downward"
@@ -737,6 +796,9 @@ class Viewer(qt.QMainWindow):
silx.config.DEFAULT_PLOT_BACKEND = "opengl"
def appendFile(self, filename):
+ if self.__displayIt is None:
+ # Store the file to display it (loading could be async)
+ self.__displayIt = filename
self.__treeview.findHdf5TreeModel().appendFile(filename)
def displaySelectedData(self):
@@ -823,7 +885,7 @@ class Viewer(qt.QMainWindow):
menu.addAction(action)
if silx.io.is_file(h5):
- action = qt.QAction("Remove %s" % obj.local_filename, event.source())
+ action = qt.QAction("Close %s" % obj.local_filename, event.source())
action.triggered.connect(lambda: self.__treeview.findHdf5TreeModel().removeH5pyObject(h5))
menu.addAction(action)
action = qt.QAction("Synchronize %s" % obj.local_filename, event.source())
diff --git a/silx/app/view/main.py b/silx/app/view/main.py
index fc89a22..90b8b17 100644
--- a/silx/app/view/main.py
+++ b/silx/app/view/main.py
@@ -25,7 +25,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "07/06/2018"
+__date__ = "17/01/2019"
import sys
import argparse
@@ -36,25 +36,6 @@ import signal
_logger = logging.getLogger(__name__)
"""Module logger"""
-if "silx.gui.qt" not in sys.modules:
- # Try first PyQt5 and not the priority imposed by silx.gui.qt.
- # To avoid problem with unittests we only do it if silx.gui.qt is not
- # yet loaded.
- # TODO: Can be removed for silx 0.8, as it should be the default binding
- # of the silx library.
- try:
- import PyQt5.QtCore
- except ImportError:
- pass
-
-import silx
-from silx.gui import qt
-
-
-def sigintHandler(*args):
- """Handler for the SIGINT signal."""
- qt.QApplication.quit()
-
def createParser():
parser = argparse.ArgumentParser(description=__doc__)
@@ -83,16 +64,8 @@ def createParser():
return parser
-def main(argv):
- """
- Main function to launch the viewer as an application
-
- :param argv: Command line arguments
- :returns: exit status
- """
- parser = createParser()
- options = parser.parse_args(argv[1:])
-
+def mainQt(options):
+ """Part of the main depending on Qt"""
if options.debug:
logging.root.setLevel(logging.DEBUG)
@@ -106,25 +79,22 @@ def main(argv):
except ImportError:
_logger.debug("Backtrace", exc_info=True)
- try:
- import h5py
- except ImportError:
- _logger.debug("Backtrace", exc_info=True)
- h5py = None
+ import h5py
- if h5py is None:
- message = "Module 'h5py' is not installed but is mandatory."\
- + " You can install it using \"pip install h5py\"."
- _logger.error(message)
- return -1
-
- #
- # Run the application
- #
+ import silx
+ import silx.utils.files
+ from silx.gui import qt
+ # Make sure matplotlib is configured
+ # Needed for Debian 8: compatibility between Qt4/Qt5 and old matplotlib
+ from silx.gui.plot import matplotlib
app = qt.QApplication([])
qt.QLocale.setDefault(qt.QLocale.c())
+ def sigintHandler(*args):
+ """Handler for the SIGINT signal."""
+ qt.QApplication.quit()
+
signal.signal(signal.SIGINT, sigintHandler)
sys.excepthook = qt.exceptionHandler
@@ -150,7 +120,11 @@ def main(argv):
# It have to be done after the settings (after the Viewer creation)
silx.config.DEFAULT_PLOT_BACKEND = "opengl"
+ # NOTE: under Windows, cmd does not convert `*.tif` into existing files
+ options.files = silx.utils.files.expand_filenames(options.files)
+
for filename in options.files:
+ # TODO: Would be nice to add a process widget and a cancel button
try:
window.appendFile(filename)
except IOError as e:
@@ -164,5 +138,17 @@ def main(argv):
return result
+def main(argv):
+ """
+ Main function to launch the viewer as an application
+
+ :param argv: Command line arguments
+ :returns: exit status
+ """
+ parser = createParser()
+ options = parser.parse_args(argv[1:])
+ mainQt(options)
+
+
if __name__ == '__main__':
main(sys.argv)
diff --git a/silx/app/view/test/test_view.py b/silx/app/view/test/test_view.py
index ebcd405..6601dce 100644
--- a/silx/app/view/test/test_view.py
+++ b/silx/app/view/test/test_view.py
@@ -35,10 +35,7 @@ import numpy
import tempfile
import shutil
import os.path
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
from silx.gui import qt
from silx.app.view.Viewer import Viewer
@@ -56,22 +53,21 @@ def setUpModule():
global _tmpDirectory
_tmpDirectory = tempfile.mkdtemp(prefix=__name__)
- if h5py is not None:
- # create h5 data
- filename = _tmpDirectory + "/data.h5"
- f = h5py.File(filename, "w")
- g = f.create_group("arrays")
- g.create_dataset("scalar", data=10)
- g.create_dataset("integers", data=numpy.array([10, 20, 30]))
- f.close()
+ # create h5 data
+ filename = _tmpDirectory + "/data.h5"
+ f = h5py.File(filename, "w")
+ g = f.create_group("arrays")
+ g.create_dataset("scalar", data=10)
+ g.create_dataset("integers", data=numpy.array([10, 20, 30]))
+ f.close()
- # create h5 data
- filename = _tmpDirectory + "/data2.h5"
- f = h5py.File(filename, "w")
- g = f.create_group("arrays")
- g.create_dataset("scalar", data=20)
- g.create_dataset("integers", data=numpy.array([10, 20, 30]))
- f.close()
+ # create h5 data
+ filename = _tmpDirectory + "/data2.h5"
+ f = h5py.File(filename, "w")
+ g = f.create_group("arrays")
+ g.create_dataset("scalar", data=20)
+ g.create_dataset("integers", data=numpy.array([10, 20, 30]))
+ f.close()
def tearDownModule():
@@ -167,7 +163,6 @@ class TestDataPanel(TestCaseQt):
self.assertIs(widget.getData(), None)
self.assertIs(widget.getCustomNxdataItem(), data)
- @unittest.skipIf(h5py is None, "Could not import h5py")
def testRemoveDatasetsFrom(self):
f = h5py.File(os.path.join(_tmpDirectory, "data.h5"))
try:
@@ -179,7 +174,6 @@ class TestDataPanel(TestCaseQt):
widget.setData(None)
f.close()
- @unittest.skipIf(h5py is None, "Could not import h5py")
def testReplaceDatasetsFrom(self):
f = h5py.File(os.path.join(_tmpDirectory, "data.h5"))
f2 = h5py.File(os.path.join(_tmpDirectory, "data2.h5"))
@@ -248,7 +242,6 @@ class TestCustomNxdataWidget(TestCaseQt):
self.assertIsNotNone(nxdata)
self.assertFalse(item.isValid())
- @unittest.skipIf(h5py is None, "Could not import h5py")
def testRemoveDatasetsFrom(self):
f = h5py.File(os.path.join(_tmpDirectory, "data.h5"))
try:
@@ -261,7 +254,6 @@ class TestCustomNxdataWidget(TestCaseQt):
model.clear()
f.close()
- @unittest.skipIf(h5py is None, "Could not import h5py")
def testReplaceDatasetsFrom(self):
f = h5py.File(os.path.join(_tmpDirectory, "data.h5"))
f2 = h5py.File(os.path.join(_tmpDirectory, "data2.h5"))
diff --git a/silx/gui/_glutils/Context.py b/silx/gui/_glutils/Context.py
index 7600992..c62dbb9 100644
--- a/silx/gui/_glutils/Context.py
+++ b/silx/gui/_glutils/Context.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -32,32 +32,44 @@ __authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "25/07/2016"
+import contextlib
-# context #####################################################################
+class _DEFAULT_CONTEXT(object):
+ """The default value for OpenGL context"""
+ pass
-def _defaultGLContextGetter():
- return None
+_context = _DEFAULT_CONTEXT
+"""The current OpenGL context"""
-_glContextGetter = _defaultGLContextGetter
-
-def getGLContext():
+def getCurrent():
"""Returns platform dependent object of current OpenGL context.
This is useful to associate OpenGL resources with the context they are
created in.
:return: Platform specific OpenGL context
- :rtype: None by default or a platform dependent object"""
- return _glContextGetter()
+ """
+ return _context
+
+
+def setCurrent(context=_DEFAULT_CONTEXT):
+ """Set a platform dependent OpenGL context
+
+ :param context: Platform dependent GL context
+ """
+ global _context
+ _context = context
-def setGLContextGetter(getter=_defaultGLContextGetter):
- """Set a platform dependent function to retrieve the current OpenGL context
+@contextlib.contextmanager
+def current(context):
+ """Context manager setting the platform-dependent GL context
- :param getter: Platform dependent GL context getter
- :type getter: Function with no args returning the current OpenGL context
+ :param context: Platform dependent GL context
"""
- global _glContextGetter
- _glContextGetter = getter
+ previous_context = getCurrent()
+ setCurrent(context)
+ yield
+ setCurrent(previous_context)
diff --git a/silx/gui/_glutils/Program.py b/silx/gui/_glutils/Program.py
index 48c12f5..87eec5f 100644
--- a/silx/gui/_glutils/Program.py
+++ b/silx/gui/_glutils/Program.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -30,11 +30,11 @@ __date__ = "25/07/2016"
import logging
+import weakref
import numpy
-from . import gl
-from .Context import getGLContext
+from . import Context, gl
_logger = logging.getLogger(__name__)
@@ -61,7 +61,7 @@ class Program(object):
self._vertexShader = vertexShader
self._fragmentShader = fragmentShader
self._attrib0 = attrib0
- self._programs = {}
+ self._programs = weakref.WeakKeyDictionary()
@staticmethod
def _compileGL(vertexShader, fragmentShader, attrib0):
@@ -106,7 +106,7 @@ class Program(object):
return program, attributes, uniforms
def _getProgramInfo(self):
- glcontext = getGLContext()
+ glcontext = Context.getCurrent()
if glcontext not in self._programs:
raise RuntimeError(
"Program was not compiled for current OpenGL context.")
@@ -149,7 +149,7 @@ class Program(object):
def use(self):
"""Make use of the program, compiling it if necessary"""
- glcontext = getGLContext()
+ glcontext = Context.getCurrent()
if glcontext not in self._programs:
self._programs[glcontext] = self._compileGL(
diff --git a/silx/gui/_glutils/Texture.py b/silx/gui/_glutils/Texture.py
index 0875ebe..a7fd44b 100644
--- a/silx/gui/_glutils/Texture.py
+++ b/silx/gui/_glutils/Texture.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,7 +29,11 @@ __license__ = "MIT"
__date__ = "04/10/2016"
-import collections
+try:
+ from collections import abc
+except ImportError: # Python2 support
+ import collections as abc
+
from ctypes import c_void_p
import logging
@@ -93,7 +97,7 @@ class Texture(object):
self.magFilter = magFilter if magFilter is not None else gl.GL_LINEAR
if wrap is not None:
- if not isinstance(wrap, collections.Iterable):
+ if not isinstance(wrap, abc.Iterable):
wrap = [wrap] * self.ndim
assert len(wrap) == self.ndim
diff --git a/silx/gui/_glutils/__init__.py b/silx/gui/_glutils/__init__.py
index 15e48e1..e88affd 100644
--- a/silx/gui/_glutils/__init__.py
+++ b/silx/gui/_glutils/__init__.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -34,9 +34,10 @@ __date__ = "25/07/2016"
# OpenGL convenient functions
from .OpenGLWidget import OpenGLWidget # noqa
-from .Context import getGLContext, setGLContextGetter # noqa
+from . import Context # noqa
from .FramebufferTexture import FramebufferTexture # noqa
from .Program import Program # noqa
from .Texture import Texture # noqa
from .VertexBuffer import VertexBuffer, VertexBufferAttrib, vertexBuffer # noqa
from .utils import sizeofGLType, isSupportedGLType, numpyToGLType # noqa
+from .utils import segmentTrianglesIntersection # noqa
diff --git a/silx/gui/_glutils/utils.py b/silx/gui/_glutils/utils.py
index 73af338..35cf819 100644
--- a/silx/gui/_glutils/utils.py
+++ b/silx/gui/_glutils/utils.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -68,3 +68,74 @@ def isSupportedGLType(type_):
def numpyToGLType(type_):
"""Returns the GL type corresponding the provided numpy type or dtype."""
return _TYPE_CONVERTER[numpy.dtype(type_)]
+
+
+def segmentTrianglesIntersection(segment, triangles):
+ """Check for segment/triangles intersection.
+
+ This is based on signed tetrahedron volume comparison.
+
+ See A. Kensler, A., Shirley, P.
+ Optimizing Ray-Triangle Intersection via Automated Search.
+ Symposium on Interactive Ray Tracing, vol. 0, p33-38 (2006)
+
+ :param numpy.ndarray segment:
+ Segment end points as a 2x3 array of coordinates
+ :param numpy.ndarray triangles:
+ Nx3x3 array of triangles
+ :return: (triangle indices, segment parameter, barycentric coord)
+ Indices of intersected triangles, "depth" along the segment
+ of the intersection point and barycentric coordinates of intersection
+ point in the triangle.
+ :rtype: List[numpy.ndarray]
+ """
+ # TODO triangles from vertices + indices
+ # TODO early rejection? e.g., check segment bbox vs triangle bbox
+ segment = numpy.asarray(segment)
+ assert segment.ndim == 2
+ assert segment.shape == (2, 3)
+
+ triangles = numpy.asarray(triangles)
+ assert triangles.ndim == 3
+ assert triangles.shape[1] == 3
+
+ # Test line/triangles intersection
+ d = segment[1] - segment[0]
+ t0s0 = segment[0] - triangles[:, 0, :]
+ edge01 = triangles[:, 1, :] - triangles[:, 0, :]
+ edge02 = triangles[:, 2, :] - triangles[:, 0, :]
+
+ dCrossEdge02 = numpy.cross(d, edge02)
+ t0s0CrossEdge01 = numpy.cross(t0s0, edge01)
+ volume = numpy.sum(dCrossEdge02 * edge01, axis=1)
+ del edge01
+ subVolumes = numpy.empty((len(triangles), 3), dtype=triangles.dtype)
+ subVolumes[:, 1] = numpy.sum(dCrossEdge02 * t0s0, axis=1)
+ del dCrossEdge02
+ subVolumes[:, 2] = numpy.sum(t0s0CrossEdge01 * d, axis=1)
+ subVolumes[:, 0] = volume - subVolumes[:, 1] - subVolumes[:, 2]
+ intersect = numpy.logical_or(
+ numpy.all(subVolumes >= 0., axis=1), # All positive
+ numpy.all(subVolumes <= 0., axis=1)) # All negative
+ intersect = numpy.where(intersect)[0] # Indices of intersected triangles
+
+ # Get barycentric coordinates
+ barycentric = subVolumes[intersect] / volume[intersect].reshape(-1, 1)
+ del subVolumes
+
+ # Test segment/triangles intersection
+ volAlpha = numpy.sum(t0s0CrossEdge01[intersect] * edge02[intersect], axis=1)
+ t = volAlpha / volume[intersect] # segment parameter of intersected triangles
+ del t0s0CrossEdge01
+ del edge02
+ del volAlpha
+ del volume
+
+ inSegmentMask = numpy.logical_and(t >= 0., t <= 1.)
+ intersect = intersect[inSegmentMask]
+ t = t[inSegmentMask]
+ barycentric = barycentric[inSegmentMask]
+
+ # Sort intersecting triangles by t
+ indices = numpy.argsort(t)
+ return intersect[indices], t[indices], barycentric[indices]
diff --git a/silx/gui/colors.py b/silx/gui/colors.py
index a51bcdc..aa2958a 100644
--- a/silx/gui/colors.py
+++ b/silx/gui/colors.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,18 +29,28 @@ from __future__ import absolute_import
__authors__ = ["T. Vincent", "H.Payno"]
__license__ = "MIT"
-__date__ = "05/10/2018"
+__date__ = "29/01/2019"
-from silx.gui import qt
-import copy as copy_mdl
import numpy
import logging
+import collections
+from silx.gui import qt
+from silx import config
from silx.math.combo import min_max
from silx.math.colormap import cmap as _cmap
from silx.utils.exceptions import NotEditableError
+from silx.utils import deprecation
+from silx.resources import resource_filename as _resource_filename
+
_logger = logging.getLogger(__file__)
+try:
+ from matplotlib import cm as _matplotlib_cm
+except ImportError:
+ _logger.info("matplotlib not available, only embedded colormaps available")
+ _matplotlib_cm = None
+
_COLORDICT = {}
"""Dictionary of common colors."""
@@ -67,12 +77,44 @@ _COLORDICT['darkBrown'] = '#660000'
_COLORDICT['darkCyan'] = '#008080'
_COLORDICT['darkYellow'] = '#808000'
_COLORDICT['darkMagenta'] = '#800080'
+_COLORDICT['transparent'] = '#00000000'
# FIXME: It could be nice to expose a functional API instead of that attribute
COLORDICT = _COLORDICT
+_LUT_DESCRIPTION = collections.namedtuple("_LUT_DESCRIPTION", ["source", "cursor_color", "preferred"])
+"""Description of a LUT for internal purpose."""
+
+
+_AVAILABLE_LUTS = collections.OrderedDict([
+ ('gray', _LUT_DESCRIPTION('builtin', 'pink', True)),
+ ('reversed gray', _LUT_DESCRIPTION('builtin', 'pink', True)),
+ ('temperature', _LUT_DESCRIPTION('builtin', 'pink', True)),
+ ('red', _LUT_DESCRIPTION('builtin', 'green', True)),
+ ('green', _LUT_DESCRIPTION('builtin', 'pink', True)),
+ ('blue', _LUT_DESCRIPTION('builtin', 'yellow', True)),
+ ('jet', _LUT_DESCRIPTION('matplotlib', 'pink', True)),
+ ('viridis', _LUT_DESCRIPTION('resource', 'pink', True)),
+ ('magma', _LUT_DESCRIPTION('resource', 'green', True)),
+ ('inferno', _LUT_DESCRIPTION('resource', 'green', True)),
+ ('plasma', _LUT_DESCRIPTION('resource', 'green', True)),
+ ('hsv', _LUT_DESCRIPTION('matplotlib', 'black', True)),
+])
+"""Description for internal porpose of all the default LUT provided by the library."""
+
+
+DEFAULT_MIN_LIN = 0
+"""Default min value if in linear normalization"""
+DEFAULT_MAX_LIN = 1
+"""Default max value if in linear normalization"""
+DEFAULT_MIN_LOG = 1
+"""Default min value if in log normalization"""
+DEFAULT_MAX_LOG = 10
+"""Default max value if in log normalization"""
+
+
def rgba(color, colorDict=None):
"""Convert color code '#RRGGBB' and '#RRGGBBAA' to (R, G, B, A)
@@ -121,19 +163,21 @@ def rgba(color, colorDict=None):
return r, g, b, a
-_COLORMAP_CURSOR_COLORS = {
- 'gray': 'pink',
- 'reversed gray': 'pink',
- 'temperature': 'pink',
- 'red': 'green',
- 'green': 'pink',
- 'blue': 'yellow',
- 'jet': 'pink',
- 'viridis': 'pink',
- 'magma': 'green',
- 'inferno': 'green',
- 'plasma': 'green',
-}
+def greyed(color, colorDict=None):
+ """Convert color code '#RRGGBB' and '#RRGGBBAA' to a grey color
+ (R, G, B, A).
+
+ It also convert RGB(A) values from uint8 to float in [0, 1] and
+ accept a QColor as color argument.
+
+ :param str color: The color to convert
+ :param dict colorDict: A dictionary of color name conversion to color code
+ :returns: RGBA colors as floats in [0., 1.]
+ :rtype: tuple
+ """
+ r, g, b, a = rgba(color=color, colorDict=colorDict)
+ g = 0.21 * r + 0.72 * g + 0.07 * b
+ return g, g, g, a
def cursorColorForColormap(colormapName):
@@ -143,26 +187,140 @@ def cursorColorForColormap(colormapName):
:return: Name of the color.
:rtype: str
"""
- return _COLORMAP_CURSOR_COLORS.get(colormapName, 'black')
+ description = _AVAILABLE_LUTS.get(colormapName, None)
+ if description is not None:
+ color = description.cursor_color
+ if color is not None:
+ return color
+ return 'black'
-DEFAULT_COLORMAPS = (
- 'gray', 'reversed gray', 'temperature', 'red', 'green', 'blue')
-"""Tuple of supported colormap names."""
+# Colormap loader
-DEFAULT_MIN_LIN = 0
-"""Default min value if in linear normalization"""
-DEFAULT_MAX_LIN = 1
-"""Default max value if in linear normalization"""
-DEFAULT_MIN_LOG = 1
-"""Default min value if in log normalization"""
-DEFAULT_MAX_LOG = 10
-"""Default max value if in log normalization"""
+_COLORMAP_CACHE = {}
+"""Cache already used colormaps as name: color LUT"""
+
+
+def _arrayToRgba8888(colors):
+ """Convert colors from a numpy array using float (0..1) int or uint
+ (0..255) to uint8 RGBA.
+
+ :param numpy.ndarray colors: Array of float int or uint colors to convert
+ :return: colors as uint8
+ :rtype: numpy.ndarray
+ """
+ assert len(colors.shape) == 2
+ assert colors.shape[1] in (3, 4)
+
+ if colors.dtype == numpy.uint8:
+ pass
+ elif colors.dtype.kind == 'f':
+ # Each bin is [N, N+1[ except the last one: [255, 256]
+ colors = numpy.clip(colors.astype(numpy.float64) * 256, 0., 255.)
+ colors = colors.astype(numpy.uint8)
+ elif colors.dtype.kind in 'iu':
+ colors = numpy.clip(colors, 0, 255)
+ colors = colors.astype(numpy.uint8)
+
+ if colors.shape[1] == 3:
+ tmp = numpy.empty((len(colors), 4), dtype=numpy.uint8)
+ tmp[:, 0:3] = colors
+ tmp[:, 3] = 255
+ colors = tmp
+
+ return colors
+
+
+def _createColormapLut(name):
+ """Returns the color LUT corresponding to a colormap name
+
+ :param str name: Name of the colormap to load
+ :returns: Corresponding table of colors
+ :rtype: numpy.ndarray
+ :raise ValueError: If no colormap corresponds to name
+ """
+ description = _AVAILABLE_LUTS.get(name)
+ use_mpl = False
+ if description is not None:
+ if description.source == "builtin":
+ # Build colormap LUT
+ lut = numpy.zeros((256, 4), dtype=numpy.uint8)
+ lut[:, 3] = 255
+
+ if name == 'gray':
+ lut[:, :3] = numpy.arange(256, dtype=numpy.uint8).reshape(-1, 1)
+ elif name == 'reversed gray':
+ lut[:, :3] = numpy.arange(255, -1, -1, dtype=numpy.uint8).reshape(-1, 1)
+ elif name == 'red':
+ lut[:, 0] = numpy.arange(256, dtype=numpy.uint8)
+ elif name == 'green':
+ lut[:, 1] = numpy.arange(256, dtype=numpy.uint8)
+ elif name == 'blue':
+ lut[:, 2] = numpy.arange(256, dtype=numpy.uint8)
+ elif name == 'temperature':
+ # Red
+ lut[128:192, 0] = numpy.arange(2, 255, 4, dtype=numpy.uint8)
+ lut[192:, 0] = 255
+ # Green
+ lut[:64, 1] = numpy.arange(0, 255, 4, dtype=numpy.uint8)
+ lut[64:192, 1] = 255
+ lut[192:, 1] = numpy.arange(252, -1, -4, dtype=numpy.uint8)
+ # Blue
+ lut[:64, 2] = 255
+ lut[64:128, 2] = numpy.arange(254, 0, -4, dtype=numpy.uint8)
+ else:
+ raise RuntimeError("Built-in colormap not implemented")
+ return lut
+
+ elif description.source == "resource":
+ # Load colormap LUT
+ colors = numpy.load(_resource_filename("gui/colormaps/%s.npy" % name))
+ # Convert to uint8 and add alpha channel
+ lut = _arrayToRgba8888(colors)
+ return lut
+
+ elif description.source == "matplotlib":
+ use_mpl = True
+
+ else:
+ raise RuntimeError("Internal LUT source '%s' unsupported" % description.source)
+
+ # Here it expect a matplotlib LUTs
+
+ if use_mpl:
+ # matplotlib is mandatory
+ if _matplotlib_cm is None:
+ raise ValueError("The colormap '%s' expect matplotlib, but matplotlib is not installed" % name)
+
+ if _matplotlib_cm is not None: # Try to load with matplotlib
+ colormap = _matplotlib_cm.get_cmap(name)
+ lut = colormap(numpy.linspace(0, 1, colormap.N, endpoint=True))
+ lut = _arrayToRgba8888(lut)
+ return lut
+
+ raise ValueError("Unknown colormap '%s'" % name)
+
+
+def _getColormap(name):
+ """Returns the color LUT corresponding to a colormap name
+
+ :param str name: Name of the colormap to load
+ :returns: Corresponding table of colors
+ :rtype: numpy.ndarray
+ :raise ValueError: If no colormap corresponds to name
+ """
+ name = str(name)
+ if name not in _COLORMAP_CACHE:
+ lut = _createColormapLut(name)
+ _COLORMAP_CACHE[name] = lut
+ return _COLORMAP_CACHE[name]
class Colormap(qt.QObject):
"""Description of a colormap
+ If no `name` nor `colors` are provided, a default gray LUT is used.
+
:param str name: Name of the colormap
:param tuple colors: optional, custom colormap.
Nx3 or Nx4 numpy array of RGB(A) colors,
@@ -187,10 +345,11 @@ class Colormap(qt.QObject):
sigChanged = qt.Signal()
"""Signal emitted when the colormap has changed."""
- def __init__(self, name='gray', colors=None, normalization=LINEAR, vmin=None, vmax=None):
+ def __init__(self, name=None, colors=None, normalization=LINEAR, vmin=None, vmax=None):
qt.QObject.__init__(self)
+ self._editable = True
+
assert normalization in Colormap.NORMALIZATIONS
- assert not (name is None and colors is None)
if normalization is Colormap.LOGARITHM:
if (vmin is not None and vmin < 0) or (vmax is not None and vmax < 0):
m = "Unsuported vmin (%s) and/or vmax (%s) given for a log scale."
@@ -200,78 +359,76 @@ class Colormap(qt.QObject):
vmin = None
vmax = None
- self._name = str(name) if name is not None else None
- self._setColors(colors)
+ self._name = None
+ self._colors = None
+
+ if colors is not None and name is not None:
+ deprecation.deprecated_warning("Argument",
+ name="silx.gui.plot.Colors",
+ reason="name and colors can't be used at the same time",
+ since_version="0.10.0",
+ skip_backtrace_count=1)
+
+ colors = None
+
+ if name is not None:
+ self.setName(name) # And resets colormap LUT
+ elif colors is not None:
+ self.setColormapLUT(colors)
+ else:
+ # Default colormap is grey
+ self.setName("gray")
+
self._normalization = str(normalization)
self._vmin = float(vmin) if vmin is not None else None
self._vmax = float(vmax) if vmax is not None else None
- self._editable = True
-
- def isAutoscale(self):
- """Return True if both min and max are in autoscale mode"""
- return self._vmin is None and self._vmax is None
- def getName(self):
- """Return the name of the colormap
- :rtype: str
- """
- return self._name
-
- @staticmethod
- def _convertColorsFromFloatToUint8(colors):
- """Convert colors from float in [0, 1] to uint8
+ def setFromColormap(self, other):
+ """Set this colormap using information from the `other` colormap.
- :param numpy.ndarray colors: Array of float colors to convert
- :return: colors as uint8
- :rtype: numpy.ndarray
+ :param Colormap other: Colormap to use as reference.
"""
- # Each bin is [N, N+1[ except the last one: [255, 256]
- return numpy.clip(
- colors.astype(numpy.float64) * 256, 0., 255.).astype(numpy.uint8)
-
- def _setColors(self, colors):
- if colors is None:
- self._colors = None
+ if not self.isEditable():
+ raise NotEditableError('Colormap is not editable')
+ if self == other:
+ return
+ old = self.blockSignals(True)
+ name = other.getName()
+ if name is not None:
+ self.setName(name)
else:
- colors = numpy.array(colors, copy=False)
- if colors.shape == ():
- raise TypeError("An array is expected for 'colors' argument. '%s' was found." % type(colors))
- colors.shape = -1, colors.shape[-1]
- if colors.dtype.kind == 'f':
- colors = self._convertColorsFromFloatToUint8(colors)
-
- # Makes sure it is RGBA8888
- self._colors = numpy.zeros((len(colors), 4), dtype=numpy.uint8)
- self._colors[:, 3] = 255 # Alpha channel
- self._colors[:, :colors.shape[1]] = colors # Copy colors
+ self.setColormapLUT(other.getColormapLUT())
+ self.setNormalization(other.getNormalization())
+ self.setVRange(other.getVMin(), other.getVMax())
+ self.blockSignals(old)
+ self.sigChanged.emit()
def getNColors(self, nbColors=None):
"""Returns N colors computed by sampling the colormap regularly.
:param nbColors:
The number of colors in the returned array or None for the default value.
- The default value is 256 for colormap with a name (see :meth:`setName`) and
- it is the size of the LUT for colormap defined with :meth:`setColormapLUT`.
+ The default value is the size of the colormap LUT.
:type nbColors: int or None
:return: 2D array of uint8 of shape (nbColors, 4)
:rtype: numpy.ndarray
"""
# Handle default value for nbColors
if nbColors is None:
- lut = self.getColormapLUT()
- if lut is not None: # In this case uses LUT length
- nbColors = len(lut)
- else: # Default to 256
- nbColors = 256
-
- nbColors = int(nbColors)
+ return numpy.array(self._colors, copy=True)
+ else:
+ colormap = self.copy()
+ colormap.setNormalization(Colormap.LINEAR)
+ colormap.setVRange(vmin=None, vmax=None)
+ colors = colormap.applyToData(
+ numpy.arange(int(nbColors), dtype=numpy.int))
+ return colors
- colormap = self.copy()
- colormap.setNormalization(Colormap.LINEAR)
- colormap.setVRange(vmin=None, vmax=None)
- colors = colormap.applyToData(
- numpy.arange(nbColors, dtype=numpy.int))
- return colors
+ def getName(self):
+ """Return the name of the colormap
+ :rtype: str
+ """
+ return self._name
def setName(self, name):
"""Set the name of the colormap to use.
@@ -281,23 +438,31 @@ class Colormap(qt.QObject):
'reversed gray', 'temperature', 'red', 'green', 'blue', 'jet',
'viridis', 'magma', 'inferno', 'plasma'.
"""
+ name = str(name)
+ if self._name == name:
+ return
if self.isEditable() is False:
raise NotEditableError('Colormap is not editable')
- assert name in self.getSupportedColormaps()
- self._name = str(name)
- self._colors = None
+ if name not in self.getSupportedColormaps():
+ raise ValueError("Colormap name '%s' is not supported" % name)
+ self._name = name
+ self._colors = _getColormap(self._name)
self.sigChanged.emit()
- def getColormapLUT(self):
- """Return the list of colors for the colormap or None if not set
+ def getColormapLUT(self, copy=True):
+ """Return the list of colors for the colormap or None if not set.
+ This returns None if the colormap was set with :meth:`setName`.
+ Use :meth:`getNColors` to get the colormap LUT for any colormap.
+
+ :param bool copy: If true a copy of the numpy array is provided
:return: the list of colors for the colormap or None if not set
:rtype: numpy.ndarray or None
"""
- if self._colors is None:
- return None
+ if self._name is None:
+ return numpy.array(self._colors, copy=copy)
else:
- return numpy.array(self._colors, copy=True)
+ return None
def setColormapLUT(self, colors):
"""Set the colors of the colormap.
@@ -310,10 +475,15 @@ class Colormap(qt.QObject):
"""
if self.isEditable() is False:
raise NotEditableError('Colormap is not editable')
- self._setColors(colors)
- if len(colors) is 0:
- self._colors = None
-
+ assert colors is not None
+
+ colors = numpy.array(colors, copy=False)
+ if colors.shape == ():
+ raise TypeError("An array is expected for 'colors' argument. '%s' was found." % type(colors))
+ assert len(colors) != 0
+ assert colors.ndim >= 2
+ colors.shape = -1, colors.shape[-1]
+ self._colors = _arrayToRgba8888(colors)
self._name = None
self.sigChanged.emit()
@@ -335,6 +505,10 @@ class Colormap(qt.QObject):
self._normalization = str(norm)
self.sigChanged.emit()
+ def isAutoscale(self):
+ """Return True if both min and max are in autoscale mode"""
+ return self._vmin is None and self._vmax is None
+
def getVMin(self):
"""Return the lower bound of the colormap
@@ -504,7 +678,7 @@ class Colormap(qt.QObject):
"""
return {
'name': self._name,
- 'colors': copy_mdl.copy(self._colors),
+ 'colors': self.getColormapLUT(),
'vmin': self._vmin,
'vmax': self._vmax,
'autoscale': self.isAutoscale(),
@@ -546,8 +720,10 @@ class Colormap(qt.QObject):
if dic.get('autoscale', False):
vmin, vmax = None, None
- self._name = name
- self._colors = colors
+ if name is not None:
+ self.setName(name)
+ else:
+ self.setColormapLUT(colors)
self._vmin = vmin
self._vmax = vmax
self._autoscale = True if (vmin is None and vmax is None) else False
@@ -557,7 +733,7 @@ class Colormap(qt.QObject):
@staticmethod
def _fromDict(dic):
- colormap = Colormap(name="")
+ colormap = Colormap()
colormap._setFromDict(dic)
return colormap
@@ -567,7 +743,7 @@ class Colormap(qt.QObject):
:rtype: silx.gui.colors.Colormap
"""
return Colormap(name=self._name,
- colors=copy_mdl.copy(self._colors),
+ colors=self.getColormapLUT(),
vmin=self._vmin,
vmax=self._vmax,
normalization=self._normalization)
@@ -577,34 +753,30 @@ class Colormap(qt.QObject):
:param numpy.ndarray data: The data to convert.
"""
- name = self.getName()
- if name is not None: # Get colormap definition from matplotlib
- # FIXME: If possible remove dependency to the plot
- from .plot.matplotlib import Colormap as MPLColormap
- mplColormap = MPLColormap.getColormap(name)
- colors = mplColormap(numpy.linspace(0, 1, 256, endpoint=True))
- colors = self._convertColorsFromFloatToUint8(colors)
-
- else: # Use user defined LUT
- colors = self.getColormapLUT()
-
vmin, vmax = self.getColormapRange(data)
normalization = self.getNormalization()
-
- return _cmap(data, colors, vmin, vmax, normalization)
+ return _cmap(data, self._colors, vmin, vmax, normalization)
@staticmethod
def getSupportedColormaps():
"""Get the supported colormap names as a tuple of str.
The list should at least contain and start by:
- ('gray', 'reversed gray', 'temperature', 'red', 'green', 'blue')
+
+ ('gray', 'reversed gray', 'temperature', 'red', 'green', 'blue',
+ 'viridis', 'magma', 'inferno', 'plasma')
+
:rtype: tuple
"""
- # FIXME: If possible remove dependency to the plot
- from .plot.matplotlib import Colormap as MPLColormap
- maps = MPLColormap.getSupportedColormaps()
- return DEFAULT_COLORMAPS + maps
+ colormaps = set()
+ if _matplotlib_cm is not None:
+ colormaps.update(_matplotlib_cm.cmap_d.keys())
+ colormaps.update(_AVAILABLE_LUTS.keys())
+
+ colormaps = tuple(cmap for cmap in sorted(colormaps)
+ if cmap not in _AVAILABLE_LUTS.keys())
+
+ return tuple(_AVAILABLE_LUTS.keys()) + colormaps
def __str__(self):
return str(self._toDict())
@@ -617,6 +789,10 @@ class Colormap(qt.QObject):
def __eq__(self, other):
"""Compare colormap values and not pointers"""
+ if other is None:
+ return False
+ if not isinstance(other, Colormap):
+ return False
return (self.getName() == other.getName() and
self.getNormalization() == other.getNormalization() and
self.getVMin() == other.getVMin() and
@@ -710,13 +886,14 @@ def preferredColormaps():
"""
global _PREFERRED_COLORMAPS
if _PREFERRED_COLORMAPS is None:
- _PREFERRED_COLORMAPS = DEFAULT_COLORMAPS
# Initialize preferred colormaps
- setPreferredColormaps(('gray', 'reversed gray',
- 'temperature', 'red', 'green', 'blue', 'jet',
- 'viridis', 'magma', 'inferno', 'plasma',
- 'hsv'))
- return _PREFERRED_COLORMAPS
+ default_preferred = []
+ for name, info in _AVAILABLE_LUTS.items():
+ if (info.preferred and
+ (info.source != 'matplotlib' or _matplotlib_cm is not None)):
+ default_preferred.append(name)
+ setPreferredColormaps(default_preferred)
+ return tuple(_PREFERRED_COLORMAPS)
def setPreferredColormaps(colormaps):
@@ -730,10 +907,41 @@ def setPreferredColormaps(colormaps):
:raise ValueError: if the list of available preferred colormaps is empty.
"""
supportedColormaps = Colormap.getSupportedColormaps()
- colormaps = tuple(
- cmap for cmap in colormaps if cmap in supportedColormaps)
+ colormaps = [cmap for cmap in colormaps if cmap in supportedColormaps]
if len(colormaps) == 0:
raise ValueError("Cannot set preferred colormaps to an empty list")
global _PREFERRED_COLORMAPS
_PREFERRED_COLORMAPS = colormaps
+
+
+def registerLUT(name, colors, cursor_color='black', preferred=True):
+ """Register a custom LUT to be used with `Colormap` objects.
+
+ It can override existing LUT names.
+
+ :param str name: Name of the LUT as defined to configure colormaps
+ :param numpy.ndarray colors: The custom LUT to register.
+ Nx3 or Nx4 numpy array of RGB(A) colors,
+ either uint8 or float in [0, 1].
+ :param bool preferred: If true, this LUT will be displayed as part of the
+ preferred colormaps in dialogs.
+ :param str cursor_color: Color used to display overlay over images using
+ colormap with this LUT.
+ """
+ description = _LUT_DESCRIPTION('user', cursor_color, preferred=preferred)
+ colors = _arrayToRgba8888(colors)
+ _AVAILABLE_LUTS[name] = description
+
+ if preferred:
+ # Invalidate the preferred cache
+ global _PREFERRED_COLORMAPS
+ if _PREFERRED_COLORMAPS is not None:
+ if name not in _PREFERRED_COLORMAPS:
+ _PREFERRED_COLORMAPS.append(name)
+ else:
+ # The cache is not yet loaded, it's fine
+ pass
+
+ # Register the cache as the LUT was already loaded
+ _COLORMAP_CACHE[name] = colors
diff --git a/silx/gui/console.py b/silx/gui/console.py
index b6341ef..5dc6336 100644
--- a/silx/gui/console.py
+++ b/silx/gui/console.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -87,17 +87,26 @@ else:
msg = "Module " + __name__ + " cannot be used within an IPython shell"
raise ImportError(msg)
-
try:
- from qtconsole.rich_ipython_widget import RichJupyterWidget as \
- RichIPythonWidget
+ from qtconsole.rich_jupyter_widget import RichJupyterWidget as \
+ _RichJupyterWidget
except ImportError:
- from qtconsole.rich_ipython_widget import RichIPythonWidget
+ try:
+ from qtconsole.rich_ipython_widget import RichJupyterWidget as \
+ _RichJupyterWidget
+ except ImportError:
+ from qtconsole.rich_ipython_widget import RichIPythonWidget as \
+ _RichJupyterWidget
from qtconsole.inprocess import QtInProcessKernelManager
+try:
+ from ipykernel import version_info as _ipykernel_version_info
+except ImportError:
+ _ipykernel_version_info = None
+
-class IPythonWidget(RichIPythonWidget):
+class IPythonWidget(_RichJupyterWidget):
"""Live IPython console widget.
.. image:: img/IPythonWidget.png
@@ -115,6 +124,16 @@ class IPythonWidget(RichIPythonWidget):
self.setWindowTitle(self.banner)
self.kernel_manager = kernel_manager = QtInProcessKernelManager()
kernel_manager.start_kernel()
+
+ # Monkey-patch to workaround issue:
+ # https://github.com/ipython/ipykernel/issues/370
+ if (_ipykernel_version_info is not None and
+ _ipykernel_version_info[0] > 4 and
+ _ipykernel_version_info[:3] <= (5, 1, 0)):
+ def _abort_queues(*args, **kwargs):
+ pass
+ kernel_manager.kernel._abort_queues = _abort_queues
+
self.kernel_client = kernel_client = self._kernel_manager.client()
kernel_client.start_channels()
@@ -178,5 +197,6 @@ def main():
widget.show()
app.exec_()
+
if __name__ == '__main__':
main()
diff --git a/silx/gui/data/ArrayTableModel.py b/silx/gui/data/ArrayTableModel.py
index ad4d33a..8805241 100644
--- a/silx/gui/data/ArrayTableModel.py
+++ b/silx/gui/data/ArrayTableModel.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -245,8 +245,7 @@ class ArrayTableModel(qt.QAbstractTableModel):
if index.isValid() and role == qt.Qt.EditRole:
try:
# cast value to same type as array
- v = numpy.asscalar(
- numpy.array(value, dtype=self._array.dtype))
+ v = numpy.array(value, dtype=self._array.dtype).item()
except ValueError:
return False
diff --git a/silx/gui/data/DataViewer.py b/silx/gui/data/DataViewer.py
index 4db2863..b33a931 100644
--- a/silx/gui/data/DataViewer.py
+++ b/silx/gui/data/DataViewer.py
@@ -32,12 +32,10 @@ from silx.gui.data.DataViews import _normalizeData
import logging
from silx.gui import qt
from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector
-from silx.utils import deprecation
-from silx.utils.property import classproperty
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "24/04/2018"
+__date__ = "12/02/2019"
_logger = logging.getLogger(__name__)
@@ -70,66 +68,6 @@ class DataViewer(qt.QFrame):
viewer.setVisible(True)
"""
- # TODO: Can be removed for silx 0.8
- @classproperty
- @deprecation.deprecated(replacement="DataViews.EMPTY_MODE", since_version="0.7", skip_backtrace_count=2)
- def EMPTY_MODE(self):
- return DataViews.EMPTY_MODE
-
- # TODO: Can be removed for silx 0.8
- @classproperty
- @deprecation.deprecated(replacement="DataViews.PLOT1D_MODE", since_version="0.7", skip_backtrace_count=2)
- def PLOT1D_MODE(self):
- return DataViews.PLOT1D_MODE
-
- # TODO: Can be removed for silx 0.8
- @classproperty
- @deprecation.deprecated(replacement="DataViews.PLOT2D_MODE", since_version="0.7", skip_backtrace_count=2)
- def PLOT2D_MODE(self):
- return DataViews.PLOT2D_MODE
-
- # TODO: Can be removed for silx 0.8
- @classproperty
- @deprecation.deprecated(replacement="DataViews.PLOT3D_MODE", since_version="0.7", skip_backtrace_count=2)
- def PLOT3D_MODE(self):
- return DataViews.PLOT3D_MODE
-
- # TODO: Can be removed for silx 0.8
- @classproperty
- @deprecation.deprecated(replacement="DataViews.RAW_MODE", since_version="0.7", skip_backtrace_count=2)
- def RAW_MODE(self):
- return DataViews.RAW_MODE
-
- # TODO: Can be removed for silx 0.8
- @classproperty
- @deprecation.deprecated(replacement="DataViews.RAW_ARRAY_MODE", since_version="0.7", skip_backtrace_count=2)
- def RAW_ARRAY_MODE(self):
- return DataViews.RAW_ARRAY_MODE
-
- # TODO: Can be removed for silx 0.8
- @classproperty
- @deprecation.deprecated(replacement="DataViews.RAW_RECORD_MODE", since_version="0.7", skip_backtrace_count=2)
- def RAW_RECORD_MODE(self):
- return DataViews.RAW_RECORD_MODE
-
- # TODO: Can be removed for silx 0.8
- @classproperty
- @deprecation.deprecated(replacement="DataViews.RAW_SCALAR_MODE", since_version="0.7", skip_backtrace_count=2)
- def RAW_SCALAR_MODE(self):
- return DataViews.RAW_SCALAR_MODE
-
- # TODO: Can be removed for silx 0.8
- @classproperty
- @deprecation.deprecated(replacement="DataViews.STACK_MODE", since_version="0.7", skip_backtrace_count=2)
- def STACK_MODE(self):
- return DataViews.STACK_MODE
-
- # TODO: Can be removed for silx 0.8
- @classproperty
- @deprecation.deprecated(replacement="DataViews.HDF5_MODE", since_version="0.7", skip_backtrace_count=2)
- def HDF5_MODE(self):
- return DataViews.HDF5_MODE
-
displayedViewChanged = qt.Signal(object)
"""Emitted when the displayed view changes"""
@@ -288,6 +226,7 @@ class DataViewer(qt.QFrame):
else:
self.__displayedData = self.__data
+ # TODO: would be good to avoid that, it should be synchonous
qt.QTimer.singleShot(10, self.__setDataInView)
def __setDataInView(self):
@@ -405,18 +344,16 @@ class DataViewer(qt.QFrame):
data = self.__data
info = self._getInfo()
# sort available views according to priority
- priorities = [v.getDataPriority(data, info) for v in self.__views]
- views = zip(priorities, self.__views)
+ views = []
+ for v in self.__views:
+ views.extend(v.getMatchingViews(data, info))
+ views = [(v.getCachedDataPriority(data, info), v) for v in views]
views = filter(lambda t: t[0] > DataViews.DataView.UNSUPPORTED, views)
views = sorted(views, reverse=True)
+ views = [v[1] for v in views]
# store available views
- if len(views) == 0:
- self.__setCurrentAvailableViews([])
- available = []
- else:
- available = [v[1] for v in views]
- self.__setCurrentAvailableViews(available)
+ self.__setCurrentAvailableViews(views)
def __updateView(self):
"""Display the data using the widget which fit the best"""
@@ -447,7 +384,7 @@ class DataViewer(qt.QFrame):
priority to lowest.
:rtype: DataView
"""
- hdf5View = self.getViewFromModeId(DataViewer.HDF5_MODE)
+ hdf5View = self.getViewFromModeId(DataViews.HDF5_MODE)
if hdf5View in available:
return hdf5View
return self.getViewFromModeId(DataViews.EMPTY_MODE)
@@ -487,6 +424,17 @@ class DataViewer(qt.QFrame):
"""
return self.__currentAvailableViews
+ def getReachableViews(self):
+ """Returns the list of reachable views from the registred available
+ views.
+
+ :rtype: List[DataView]
+ """
+ views = []
+ for v in self.availableViews():
+ views.extend(v.getReachableViews())
+ return views
+
def availableViews(self):
"""Returns the list of registered views
diff --git a/silx/gui/data/DataViewerFrame.py b/silx/gui/data/DataViewerFrame.py
index 4e6d2e8..9bfb95b 100644
--- a/silx/gui/data/DataViewerFrame.py
+++ b/silx/gui/data/DataViewerFrame.py
@@ -27,7 +27,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "24/04/2018"
+__date__ = "12/02/2019"
from silx.gui import qt
from .DataViewer import DataViewer
@@ -120,6 +120,9 @@ class DataViewerFrame(qt.QWidget):
"""
self.__dataViewer.setGlobalHooks(hooks)
+ def getReachableViews(self):
+ return self.__dataViewer.getReachableViews()
+
def availableViews(self):
"""Returns the list of registered views
diff --git a/silx/gui/data/DataViewerSelector.py b/silx/gui/data/DataViewerSelector.py
index 35bbe99..a1e9947 100644
--- a/silx/gui/data/DataViewerSelector.py
+++ b/silx/gui/data/DataViewerSelector.py
@@ -29,7 +29,7 @@ from __future__ import division
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "23/01/2018"
+__date__ = "12/02/2019"
import weakref
import functools
@@ -85,7 +85,7 @@ class DataViewerSelector(qt.QWidget):
iconSize = qt.QSize(16, 16)
- for view in self.__dataViewer.availableViews():
+ for view in self.__dataViewer.getReachableViews():
label = view.label()
icon = view.icon()
button = qt.QPushButton(label)
@@ -155,7 +155,7 @@ class DataViewerSelector(qt.QWidget):
self.__dataViewer.setDisplayedView(view)
def __checkAvailableButtons(self):
- views = set(self.__dataViewer.availableViews())
+ views = set(self.__dataViewer.getReachableViews())
if views == set(self.__buttons.keys()):
return
# Recreate all the buttons
diff --git a/silx/gui/data/DataViews.py b/silx/gui/data/DataViews.py
index 2291e87..664090d 100644
--- a/silx/gui/data/DataViews.py
+++ b/silx/gui/data/DataViews.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -31,6 +31,7 @@ import numbers
import numpy
import silx.io
+from silx.utils import deprecation
from silx.gui import qt, icons
from silx.gui.data.TextFormatter import TextFormatter
from silx.io import nxdata
@@ -41,7 +42,7 @@ from silx.gui.dialog.ColormapDialog import ColormapDialog
__authors__ = ["V. Valls", "P. Knobel"]
__license__ = "MIT"
-__date__ = "23/05/2018"
+__date__ = "19/02/2019"
_logger = logging.getLogger(__name__)
@@ -67,6 +68,8 @@ NXDATA_CURVE_MODE = 73
NXDATA_XYVSCATTER_MODE = 74
NXDATA_IMAGE_MODE = 75
NXDATA_STACK_MODE = 76
+NXDATA_VOLUME_MODE = 77
+NXDATA_VOLUME_AS_STACK_MODE = 78
def _normalizeData(data):
@@ -100,6 +103,7 @@ class DataInfo(object):
"""Store extracted information from a data"""
def __init__(self, data):
+ self.__priorities = {}
data = self.normalizeData(data)
self.isArray = False
self.interpretation = None
@@ -131,9 +135,6 @@ class DataInfo(object):
elif nx_class == "NXdata":
# group claiming to be NXdata could not be parsed
self.isInvalidNXdata = True
- elif nx_class == "NXentry" and "default" in data.attrs:
- # entry claiming to have a default NXdata could not be parsed
- self.isInvalidNXdata = True
elif nx_class == "NXroot" or silx.io.is_file(data):
# root claiming to have a default entry
if "default" in data.attrs:
@@ -141,6 +142,9 @@ class DataInfo(object):
if def_entry in data and "default" in data[def_entry].attrs:
# and entry claims to have default NXdata
self.isInvalidNXdata = True
+ elif "default" in data.attrs:
+ # group claiming to have a default NXdata could not be parsed
+ self.isInvalidNXdata = True
if isinstance(data, numpy.ndarray):
self.isArray = True
@@ -201,6 +205,12 @@ class DataInfo(object):
Else returns the data."""
return _normalizeData(data)
+ def cachePriority(self, view, priority):
+ self.__priorities[view] = priority
+
+ def getPriority(self, view):
+ return self.__priorities[view]
+
class DataViewHooks(object):
"""A set of hooks defined to custom the behaviour of the data views."""
@@ -357,6 +367,35 @@ class DataView(object):
"""
return []
+ def getReachableViews(self):
+ """Returns the views that can be returned by `getMatchingViews`.
+
+ :param object data: Any object to be displayed
+ :param DataInfo info: Information cached about this data
+ :rtype: List[DataView]
+ """
+ return [self]
+
+ def getMatchingViews(self, data, info):
+ """Returns the views according to data and info from the data.
+
+ :param object data: Any object to be displayed
+ :param DataInfo info: Information cached about this data
+ :rtype: List[DataView]
+ """
+ priority = self.getCachedDataPriority(data, info)
+ if priority == DataView.UNSUPPORTED:
+ return []
+ return [self]
+
+ def getCachedDataPriority(self, data, info):
+ try:
+ priority = info.getPriority(self)
+ except KeyError:
+ priority = self.getDataPriority(data, info)
+ info.cachePriority(self, priority)
+ return priority
+
def getDataPriority(self, data, info):
"""
Returns the priority of using this view according to a data.
@@ -377,7 +416,53 @@ class DataView(object):
return str(self) < str(other)
-class CompositeDataView(DataView):
+class _CompositeDataView(DataView):
+ """Contains sub views"""
+
+ def getViews(self):
+ """Returns the direct sub views registered in this view.
+
+ :rtype: List[DataView]
+ """
+ raise NotImplementedError()
+
+ def getReachableViews(self):
+ """Returns all views that can be reachable at on point.
+
+ This method return any sub view provided (recursivly).
+
+ :rtype: List[DataView]
+ """
+ raise NotImplementedError()
+
+ def getMatchingViews(self, data, info):
+ """Returns sub views matching this data and info.
+
+ This method return any sub view provided (recursivly).
+
+ :param object data: Any object to be displayed
+ :param DataInfo info: Information cached about this data
+ :rtype: List[DataView]
+ """
+ raise NotImplementedError()
+
+ @deprecation.deprecated(replacement="getReachableViews", since_version="0.10")
+ def availableViews(self):
+ return self.getViews()
+
+ def isSupportedData(self, data, info):
+ """If true, the composite view allow sub views to access to this data.
+ Else this this data is considered as not supported by any of sub views
+ (incliding this composite view).
+
+ :param object data: Any object to be displayed
+ :param DataInfo info: Information cached about this data
+ :rtype: bool
+ """
+ return True
+
+
+class SelectOneDataView(_CompositeDataView):
"""Data view which can display a data using different view according to
the kind of the data."""
@@ -386,7 +471,7 @@ class CompositeDataView(DataView):
:param qt.QWidget parent: Parent of the hold widget
"""
- super(CompositeDataView, self).__init__(parent, modeId, icon, label)
+ super(SelectOneDataView, self).__init__(parent, modeId, icon, label)
self.__views = OrderedDict()
self.__currentView = None
@@ -395,7 +480,7 @@ class CompositeDataView(DataView):
:param DataViewHooks hooks: The data view hooks to use
"""
- super(CompositeDataView, self).setHooks(hooks)
+ super(SelectOneDataView, self).setHooks(hooks)
if hooks is not None:
for v in self.__views:
v.setHooks(hooks)
@@ -407,16 +492,40 @@ class CompositeDataView(DataView):
dataView.setHooks(hooks)
self.__views[dataView] = None
- def availableViews(self):
+ def getReachableViews(self):
+ views = []
+ addSelf = False
+ for v in self.__views:
+ if isinstance(v, SelectManyDataView):
+ views.extend(v.getReachableViews())
+ else:
+ addSelf = True
+ if addSelf:
+ # Single views are hidden by this view
+ views.insert(0, self)
+ return views
+
+ def getMatchingViews(self, data, info):
+ if not self.isSupportedData(data, info):
+ return []
+ view = self.__getBestView(data, info)
+ if isinstance(view, SelectManyDataView):
+ return view.getMatchingViews(data, info)
+ else:
+ return [self]
+
+ def getViews(self):
"""Returns the list of registered views
:rtype: List[DataView]
"""
return list(self.__views.keys())
- def getBestView(self, data, info):
+ def __getBestView(self, data, info):
"""Returns the best view according to priorities."""
- views = [(v.getDataPriority(data, info), v) for v in self.__views.keys()]
+ if not self.isSupportedData(data, info):
+ return None
+ views = [(v.getCachedDataPriority(data, info), v) for v in self.__views.keys()]
views = filter(lambda t: t[0] > DataView.UNSUPPORTED, views)
views = sorted(views, key=lambda t: t[0], reverse=True)
@@ -471,17 +580,17 @@ class CompositeDataView(DataView):
self.__currentView.setData(data)
def axesNames(self, data, info):
- view = self.getBestView(data, info)
+ view = self.__getBestView(data, info)
self.__currentView = view
return view.axesNames(data, info)
def getDataPriority(self, data, info):
- view = self.getBestView(data, info)
+ view = self.__getBestView(data, info)
self.__currentView = view
if view is None:
return DataView.UNSUPPORTED
else:
- return view.getDataPriority(data, info)
+ return view.getCachedDataPriority(data, info)
def replaceView(self, modeId, newView):
"""Replace a data view with a custom view.
@@ -502,7 +611,7 @@ class CompositeDataView(DataView):
if view.modeId() == modeId:
oldView = view
break
- elif isinstance(view, CompositeDataView):
+ elif isinstance(view, _CompositeDataView):
# recurse
hooks = self.getHooks()
if hooks is not None:
@@ -519,6 +628,135 @@ class CompositeDataView(DataView):
return True
+# NOTE: SelectOneDataView was introduced with silx 0.10
+CompositeDataView = SelectOneDataView
+
+
+class SelectManyDataView(_CompositeDataView):
+ """Data view which can select a set of sub views according to
+ the kind of the data.
+
+ This view itself is abstract and is not exposed.
+ """
+
+ def __init__(self, parent, views=None):
+ """Constructor
+
+ :param qt.QWidget parent: Parent of the hold widget
+ """
+ super(SelectManyDataView, self).__init__(parent, modeId=None, icon=None, label=None)
+ if views is None:
+ views = []
+ self.__views = views
+
+ def setHooks(self, hooks):
+ """Set the data context to use with this view.
+
+ :param DataViewHooks hooks: The data view hooks to use
+ """
+ super(SelectManyDataView, self).setHooks(hooks)
+ if hooks is not None:
+ for v in self.__views:
+ v.setHooks(hooks)
+
+ def addView(self, dataView):
+ """Add a new dataview to the available list."""
+ hooks = self.getHooks()
+ if hooks is not None:
+ dataView.setHooks(hooks)
+ self.__views.append(dataView)
+
+ def getViews(self):
+ """Returns the list of registered views
+
+ :rtype: List[DataView]
+ """
+ return list(self.__views)
+
+ def getReachableViews(self):
+ views = []
+ for v in self.__views:
+ views.extend(v.getReachableViews())
+ return views
+
+ def getMatchingViews(self, data, info):
+ """Returns the views according to data and info from the data.
+
+ :param object data: Any object to be displayed
+ :param DataInfo info: Information cached about this data
+ """
+ if not self.isSupportedData(data, info):
+ return []
+ views = [v for v in self.__views if v.getCachedDataPriority(data, info) != DataView.UNSUPPORTED]
+ return views
+
+ def customAxisNames(self):
+ raise RuntimeError("Abstract view")
+
+ def setCustomAxisValue(self, name, value):
+ raise RuntimeError("Abstract view")
+
+ def select(self):
+ raise RuntimeError("Abstract view")
+
+ def createWidget(self, parent):
+ raise RuntimeError("Abstract view")
+
+ def clear(self):
+ for v in self.__views:
+ v.clear()
+
+ def setData(self, data):
+ raise RuntimeError("Abstract view")
+
+ def axesNames(self, data, info):
+ raise RuntimeError("Abstract view")
+
+ def getDataPriority(self, data, info):
+ if not self.isSupportedData(data, info):
+ return DataView.UNSUPPORTED
+ priorities = [v.getCachedDataPriority(data, info) for v in self.__views]
+ priorities = [v for v in priorities if v != DataView.UNSUPPORTED]
+ priorities = sorted(priorities)
+ if len(priorities) == 0:
+ return DataView.UNSUPPORTED
+ return priorities[-1]
+
+ def replaceView(self, modeId, newView):
+ """Replace a data view with a custom view.
+ Return True in case of success, False in case of failure.
+
+ .. note::
+
+ This method must be called just after instantiation, before
+ the viewer is used.
+
+ :param int modeId: Unique mode ID identifying the DataView to
+ be replaced.
+ :param DataViews.DataView newView: New data view
+ :return: True if replacement was successful, else False
+ """
+ oldView = None
+ for iview, view in enumerate(self.__views):
+ if view.modeId() == modeId:
+ oldView = view
+ break
+ elif isinstance(view, CompositeDataView):
+ # recurse
+ hooks = self.getHooks()
+ if hooks is not None:
+ newView.setHooks(hooks)
+ if view.replaceView(modeId, newView):
+ return True
+
+ if oldView is None:
+ return False
+
+ # replace oldView with new view in dict
+ self.__views[iview] = newView
+ return True
+
+
class _EmptyView(DataView):
"""Dummy view to display nothing"""
@@ -655,53 +893,27 @@ class _Plot3dView(DataView):
label="Cube",
icon=icons.getQIcon("view-3d"))
try:
- import silx.gui.plot3d #noqa
+ from ._VolumeWindow import VolumeWindow # noqa
except ImportError:
- _logger.warning("Plot3dView is not available")
+ _logger.warning("3D visualization is not available")
_logger.debug("Backtrace", exc_info=True)
raise
self.__resetZoomNextTime = True
def createWidget(self, parent):
- from silx.gui.plot3d import ScalarFieldView
- from silx.gui.plot3d import SFViewParamTree
+ from ._VolumeWindow import VolumeWindow
- plot = ScalarFieldView.ScalarFieldView(parent)
+ plot = VolumeWindow(parent)
plot.setAxesLabels(*reversed(self.axesNames(None, None)))
-
- def computeIsolevel(data):
- data = data[numpy.isfinite(data)]
- if len(data) == 0:
- return 0
- else:
- return numpy.mean(data) + numpy.std(data)
-
- plot.addIsosurface(computeIsolevel, '#FF0000FF')
-
- # Create a parameter tree for the scalar field view
- options = SFViewParamTree.TreeView(plot)
- options.setSfView(plot)
-
- # Add the parameter tree to the main window in a dock widget
- dock = qt.QDockWidget()
- dock.setWidget(options)
- plot.addDockWidget(qt.Qt.RightDockWidgetArea, dock)
-
return plot
def clear(self):
- self.getWidget().setData(None)
+ self.getWidget().clear()
self.__resetZoomNextTime = True
- def normalizeData(self, data):
- data = DataView.normalizeData(self, data)
- data = _normalizeComplex(data)
- return data
-
def setData(self, data):
data = self.normalizeData(data)
- plot = self.getWidget()
- plot.setData(data)
+ self.getWidget().setData(data)
self.__resetZoomNextTime = False
def axesNames(self, data, info):
@@ -735,10 +947,10 @@ class _ComplexImageView(DataView):
def createWidget(self, parent):
from silx.gui.plot.ComplexImageView import ComplexImageView
widget = ComplexImageView(parent=parent)
- widget.setColormap(self.defaultColormap(), mode=ComplexImageView.Mode.ABSOLUTE)
- widget.setColormap(self.defaultColormap(), mode=ComplexImageView.Mode.SQUARE_AMPLITUDE)
- widget.setColormap(self.defaultColormap(), mode=ComplexImageView.Mode.REAL)
- widget.setColormap(self.defaultColormap(), mode=ComplexImageView.Mode.IMAGINARY)
+ widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.ABSOLUTE)
+ widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.SQUARE_AMPLITUDE)
+ widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.REAL)
+ widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.IMAGINARY)
widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog())
widget.getPlot().getIntensityHistogramAction().setVisible(True)
widget.getPlot().setKeepDataAspectRatio(True)
@@ -1096,17 +1308,6 @@ class _InvalidNXdataView(DataView):
# invalid: could not even be parsed by NXdata
self._msg = "Group has @NX_class = NXdata, but could not be interpreted"
self._msg += " as valid NXdata."
- elif nx_class == "NXentry":
- self._msg = "NXentry group provides a @default attribute,"
- default_nxdata_name = data.attrs["default"]
- if default_nxdata_name not in data:
- self._msg += " but no corresponding NXdata group exists."
- elif get_attr_as_unicode(data[default_nxdata_name], "NX_class") != "NXdata":
- self._msg += " but the corresponding item is not a "
- self._msg += "NXdata group."
- else:
- self._msg += " but the corresponding NXdata seems to be"
- self._msg += " malformed."
elif nx_class == "NXroot" or silx.io.is_file(data):
default_entry = data[data.attrs["default"]]
default_nxdata_name = default_entry.attrs["default"]
@@ -1122,6 +1323,17 @@ class _InvalidNXdataView(DataView):
else:
self._msg += " but the corresponding NXdata seems to be"
self._msg += " malformed."
+ else:
+ self._msg = "Group provides a @default attribute,"
+ default_nxdata_name = data.attrs["default"]
+ if default_nxdata_name not in data:
+ self._msg += " but no corresponding NXdata group exists."
+ elif get_attr_as_unicode(data[default_nxdata_name], "NX_class") != "NXdata":
+ self._msg += " but the corresponding item is not a "
+ self._msg += "NXdata group."
+ else:
+ self._msg += " but the corresponding NXdata seems to be"
+ self._msg += " malformed."
return 100
@@ -1277,7 +1489,7 @@ class _NXdataXYVScatterView(DataView):
class _NXdataImageView(DataView):
"""DataView using a Plot2D for displaying NXdata images:
- 2-D signal or n-D signals with *@interpretation=spectrum*."""
+ 2-D signal or n-D signals with *@interpretation=image*."""
def __init__(self, parent):
DataView.__init__(self, parent,
modeId=NXDATA_IMAGE_MODE)
@@ -1323,6 +1535,53 @@ class _NXdataImageView(DataView):
return DataView.UNSUPPORTED
+class _NXdataComplexImageView(DataView):
+ """DataView using a ComplexImageView for displaying NXdata complex images:
+ 2-D signal or n-D signals with *@interpretation=image*."""
+ def __init__(self, parent):
+ DataView.__init__(self, parent,
+ modeId=NXDATA_IMAGE_MODE)
+
+ def createWidget(self, parent):
+ from silx.gui.data.NXdataWidgets import ArrayComplexImagePlot
+ widget = ArrayComplexImagePlot(parent, colormap=self.defaultColormap())
+ widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog())
+ return widget
+
+ def clear(self):
+ self.getWidget().clear()
+
+ def setData(self, data):
+ data = self.normalizeData(data)
+ nxd = nxdata.get_default(data, validate=False)
+
+ # last two axes are Y & X
+ img_slicing = slice(-2, None)
+ y_axis, x_axis = nxd.axes[img_slicing]
+ y_label, x_label = nxd.axes_names[img_slicing]
+
+ self.getWidget().setImageData(
+ [nxd.signal] + nxd.auxiliary_signals,
+ x_axis=x_axis, y_axis=y_axis,
+ signals_names=[nxd.signal_name] + nxd.auxiliary_signals_names,
+ xlabel=x_label, ylabel=y_label,
+ title=nxd.title)
+
+ def axesNames(self, data, info):
+ # disabled (used by default axis selector widget in Hdf5Viewer)
+ return None
+
+ def getDataPriority(self, data, info):
+ data = self.normalizeData(data)
+
+ if info.hasNXdata and not info.isInvalidNXdata:
+ nxd = nxdata.get_default(data, validate=False)
+ if nxd.is_image and numpy.iscomplexobj(nxd.signal):
+ return 100
+
+ return DataView.UNSUPPORTED
+
+
class _NXdataStackView(DataView):
def __init__(self, parent):
DataView.__init__(self, parent,
@@ -1368,6 +1627,154 @@ class _NXdataStackView(DataView):
return DataView.UNSUPPORTED
+class _NXdataVolumeView(DataView):
+ def __init__(self, parent):
+ DataView.__init__(self, parent,
+ label="NXdata (3D)",
+ icon=icons.getQIcon("view-nexus"),
+ modeId=NXDATA_VOLUME_MODE)
+ try:
+ import silx.gui.plot3d # noqa
+ except ImportError:
+ _logger.warning("Plot3dView is not available")
+ _logger.debug("Backtrace", exc_info=True)
+ raise
+
+ def normalizeData(self, data):
+ data = DataView.normalizeData(self, data)
+ data = _normalizeComplex(data)
+ return data
+
+ def createWidget(self, parent):
+ from silx.gui.data.NXdataWidgets import ArrayVolumePlot
+ widget = ArrayVolumePlot(parent)
+ return widget
+
+ def axesNames(self, data, info):
+ # disabled (used by default axis selector widget in Hdf5Viewer)
+ return None
+
+ def clear(self):
+ self.getWidget().clear()
+
+ def setData(self, data):
+ data = self.normalizeData(data)
+ nxd = nxdata.get_default(data, validate=False)
+ signal_name = nxd.signal_name
+ z_axis, y_axis, x_axis = nxd.axes[-3:]
+ z_label, y_label, x_label = nxd.axes_names[-3:]
+ title = nxd.title or signal_name
+
+ widget = self.getWidget()
+ widget.setData(
+ nxd.signal, x_axis=x_axis, y_axis=y_axis, z_axis=z_axis,
+ signal_name=signal_name,
+ xlabel=x_label, ylabel=y_label, zlabel=z_label,
+ title=title)
+
+ def getDataPriority(self, data, info):
+ data = self.normalizeData(data)
+ if info.hasNXdata and not info.isInvalidNXdata:
+ if nxdata.get_default(data, validate=False).is_volume:
+ return 150
+
+ return DataView.UNSUPPORTED
+
+
+class _NXdataVolumeAsStackView(DataView):
+ def __init__(self, parent):
+ DataView.__init__(self, parent,
+ label="NXdata (2D)",
+ icon=icons.getQIcon("view-nexus"),
+ modeId=NXDATA_VOLUME_AS_STACK_MODE)
+
+ def createWidget(self, parent):
+ from silx.gui.data.NXdataWidgets import ArrayStackPlot
+ widget = ArrayStackPlot(parent)
+ widget.getStackView().setColormap(self.defaultColormap())
+ widget.getStackView().getPlot().getColormapAction().setColorDialog(self.defaultColorDialog())
+ return widget
+
+ def axesNames(self, data, info):
+ # disabled (used by default axis selector widget in Hdf5Viewer)
+ return None
+
+ def clear(self):
+ self.getWidget().clear()
+
+ def setData(self, data):
+ data = self.normalizeData(data)
+ nxd = nxdata.get_default(data, validate=False)
+ signal_name = nxd.signal_name
+ z_axis, y_axis, x_axis = nxd.axes[-3:]
+ z_label, y_label, x_label = nxd.axes_names[-3:]
+ title = nxd.title or signal_name
+
+ widget = self.getWidget()
+ widget.setStackData(
+ nxd.signal, x_axis=x_axis, y_axis=y_axis, z_axis=z_axis,
+ signal_name=signal_name,
+ xlabel=x_label, ylabel=y_label, zlabel=z_label,
+ title=title)
+ # Override the colormap, while setStack overwrite it
+ widget.getStackView().setColormap(self.defaultColormap())
+
+ def getDataPriority(self, data, info):
+ data = self.normalizeData(data)
+ if info.isComplex:
+ return DataView.UNSUPPORTED
+ if info.hasNXdata and not info.isInvalidNXdata:
+ if nxdata.get_default(data, validate=False).is_volume:
+ return 200
+
+ return DataView.UNSUPPORTED
+
+class _NXdataComplexVolumeAsStackView(DataView):
+ def __init__(self, parent):
+ DataView.__init__(self, parent,
+ label="NXdata (2D)",
+ icon=icons.getQIcon("view-nexus"),
+ modeId=NXDATA_VOLUME_AS_STACK_MODE)
+ self._is_complex_data = False
+
+ def createWidget(self, parent):
+ from silx.gui.data.NXdataWidgets import ArrayComplexImagePlot
+ widget = ArrayComplexImagePlot(parent, colormap=self.defaultColormap())
+ widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog())
+ return widget
+
+ def axesNames(self, data, info):
+ # disabled (used by default axis selector widget in Hdf5Viewer)
+ return None
+
+ def clear(self):
+ self.getWidget().clear()
+
+ def setData(self, data):
+ data = self.normalizeData(data)
+ nxd = nxdata.get_default(data, validate=False)
+ signal_name = nxd.signal_name
+ z_axis, y_axis, x_axis = nxd.axes[-3:]
+ z_label, y_label, x_label = nxd.axes_names[-3:]
+ title = nxd.title or signal_name
+
+ self.getWidget().setImageData(
+ [nxd.signal] + nxd.auxiliary_signals,
+ x_axis=x_axis, y_axis=y_axis,
+ signals_names=[nxd.signal_name] + nxd.auxiliary_signals_names,
+ xlabel=x_label, ylabel=y_label, title=nxd.title)
+
+ def getDataPriority(self, data, info):
+ data = self.normalizeData(data)
+ if not info.isComplex:
+ return DataView.UNSUPPORTED
+ if info.hasNXdata and not info.isInvalidNXdata:
+ if nxdata.get_default(data, validate=False).is_volume:
+ return 200
+
+ return DataView.UNSUPPORTED
+
+
class _NXdataView(CompositeDataView):
"""Composite view displaying NXdata groups using the most adequate
widget depending on the dimensionality."""
@@ -1382,5 +1789,17 @@ class _NXdataView(CompositeDataView):
self.addView(_NXdataScalarView(parent))
self.addView(_NXdataCurveView(parent))
self.addView(_NXdataXYVScatterView(parent))
+ self.addView(_NXdataComplexImageView(parent))
self.addView(_NXdataImageView(parent))
self.addView(_NXdataStackView(parent))
+
+ # The 3D view can be displayed using 2 ways
+ nx3dViews = SelectManyDataView(parent)
+ nx3dViews.addView(_NXdataVolumeAsStackView(parent))
+ nx3dViews.addView(_NXdataComplexVolumeAsStackView(parent))
+ try:
+ nx3dViews.addView(_NXdataVolumeView(parent))
+ except Exception:
+ _logger.warning("NXdataVolumeView is not available")
+ _logger.debug("Backtrace", exc_info=True)
+ self.addView(nx3dViews)
diff --git a/silx/gui/data/Hdf5TableView.py b/silx/gui/data/Hdf5TableView.py
index 9e28fbf..d7c33f3 100644
--- a/silx/gui/data/Hdf5TableView.py
+++ b/silx/gui/data/Hdf5TableView.py
@@ -30,12 +30,14 @@ from __future__ import division
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "05/07/2018"
+__date__ = "12/02/2019"
import collections
import functools
import os.path
import logging
+import h5py
+
from silx.gui import qt
import silx.io
from .TextFormatter import TextFormatter
@@ -44,11 +46,6 @@ from silx.gui.widgets import HierarchicalTableView
from ..hdf5.Hdf5Formatter import Hdf5Formatter
from ..hdf5._utils import htmlFromDict
-try:
- import h5py
-except ImportError:
- h5py = None
-
_logger = logging.getLogger(__name__)
@@ -198,11 +195,9 @@ class _CellFilterAvailableData(_CellData):
}
def __init__(self, filterId):
- import h5py.version
if h5py.version.hdf5_version_tuple >= (1, 10, 2):
# Previous versions only returns True if the filter was first used
# to decode a dataset
- import h5py.h5z
self.__availability = h5py.h5z.filter_avail(filterId)
else:
self.__availability = "na"
@@ -416,7 +411,7 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
self.__data.addHeaderRow(headerLabel="Data info")
- if h5py is not None and hasattr(obj, "id") and hasattr(obj.id, "get_type"):
+ if hasattr(obj, "id") and hasattr(obj.id, "get_type"):
# display the HDF5 type
self.__data.addHeaderValueRow("HDF5 type", self.__formatHdf5Type)
self.__data.addHeaderValueRow("dtype", self.__formatDType)
diff --git a/silx/gui/data/HexaTableView.py b/silx/gui/data/HexaTableView.py
index c86c0af..1617f0a 100644
--- a/silx/gui/data/HexaTableView.py
+++ b/silx/gui/data/HexaTableView.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -28,11 +28,13 @@ hexadecimal viewer.
"""
from __future__ import division
-import numpy
import collections
+
+import numpy
+import six
+
from silx.gui import qt
import silx.io.utils
-from silx.third_party import six
from silx.gui.widgets.TableWidget import CopySelectedCellsAction
__authors__ = ["V. Valls"]
diff --git a/silx/gui/data/NXdataWidgets.py b/silx/gui/data/NXdataWidgets.py
index f7c479d..c3aefd3 100644
--- a/silx/gui/data/NXdataWidgets.py
+++ b/silx/gui/data/NXdataWidgets.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -26,19 +26,24 @@
"""
__authors__ = ["P. Knobel"]
__license__ = "MIT"
-__date__ = "10/10/2018"
+__date__ = "12/11/2018"
+import logging
import numpy
from silx.gui import qt
from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector
from silx.gui.plot import Plot1D, Plot2D, StackView, ScatterView
+from silx.gui.plot.ComplexImageView import ComplexImageView
from silx.gui.colors import Colormap
from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser
from silx.math.calibration import ArrayCalibration, NoCalibration, LinearCalibration
+_logger = logging.getLogger(__name__)
+
+
class ArrayCurvePlot(qt.QWidget):
"""
Widget for plotting a curve from a multi-dimensional signal array
@@ -72,21 +77,16 @@ class ArrayCurvePlot(qt.QWidget):
self._plot = Plot1D(self)
- self.selectorDock = qt.QDockWidget("Data selector", self._plot)
- # not closable
- self.selectorDock.setFeatures(qt.QDockWidget.DockWidgetMovable |
- qt.QDockWidget.DockWidgetFloatable)
- self._selector = NumpyAxesSelector(self.selectorDock)
+ self._selector = NumpyAxesSelector(self)
self._selector.setNamedAxesSelectorVisibility(False)
self.__selector_is_connected = False
- self.selectorDock.setWidget(self._selector)
- self._plot.addTabbedDockWidget(self.selectorDock)
self._plot.sigActiveCurveChanged.connect(self._setYLabelFromActiveLegend)
- layout = qt.QGridLayout()
+ layout = qt.QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
- layout.addWidget(self._plot, 0, 0)
+ layout.addWidget(self._plot)
+ layout.addWidget(self._selector)
self.setLayout(layout)
@@ -130,9 +130,9 @@ class ArrayCurvePlot(qt.QWidget):
self._selector.setAxisNames(["Y"])
if len(ys[0].shape) < 2:
- self.selectorDock.hide()
+ self._selector.hide()
else:
- self.selectorDock.show()
+ self._selector.show()
self._plot.setGraphTitle(title or "")
self._updateCurve()
@@ -182,6 +182,9 @@ class ArrayCurvePlot(qt.QWidget):
break
def clear(self):
+ old = self._selector.blockSignals(True)
+ self._selector.clear()
+ self._selector.blockSignals(old)
self._plot.clear()
@@ -339,11 +342,8 @@ class ArrayImagePlot(qt.QWidget):
normalization=Colormap.LINEAR))
self._plot.getIntensityHistogramAction().setVisible(True)
- self.selectorDock = qt.QDockWidget("Data selector", self._plot)
# not closable
- self.selectorDock.setFeatures(qt.QDockWidget.DockWidgetMovable |
- qt.QDockWidget.DockWidgetFloatable)
- self._selector = NumpyAxesSelector(self.selectorDock)
+ self._selector = NumpyAxesSelector(self)
self._selector.setNamedAxesSelectorVisibility(False)
self._selector.selectionChanged.connect(self._updateImage)
@@ -355,9 +355,8 @@ class ArrayImagePlot(qt.QWidget):
layout = qt.QVBoxLayout()
layout.addWidget(self._plot)
+ layout.addWidget(self._selector)
layout.addWidget(self._auxSigSlider)
- self.selectorDock.setWidget(self._selector)
- self._plot.addTabbedDockWidget(self.selectorDock)
self.setLayout(layout)
@@ -413,9 +412,9 @@ class ArrayImagePlot(qt.QWidget):
self._selector.setData(signals[0])
if len(signals[0].shape) <= img_ndim:
- self.selectorDock.hide()
+ self._selector.hide()
else:
- self.selectorDock.show()
+ self._selector.show()
self._auxSigSlider.setMaximum(len(signals) - 1)
if len(signals) > 1:
@@ -425,6 +424,7 @@ class ArrayImagePlot(qt.QWidget):
self._auxSigSlider.setValue(0)
self._updateImage()
+ self._plot.resetZoom()
self._selector.selectionChanged.connect(self._updateImage)
self._auxSigSlider.valueChanged.connect(self._sliderIdxChanged)
@@ -492,12 +492,202 @@ class ArrayImagePlot(qt.QWidget):
self._plot.setGraphTitle(title)
self._plot.getXAxis().setLabel(self.__x_axis_name)
self._plot.getYAxis().setLabel(self.__y_axis_name)
- self._plot.resetZoom()
def clear(self):
+ old = self._selector.blockSignals(True)
+ self._selector.clear()
+ self._selector.blockSignals(old)
self._plot.clear()
+class ArrayComplexImagePlot(qt.QWidget):
+ """
+ Widget for plotting an image of complex from a multi-dimensional signal array
+ and two 1D axes array.
+
+ The signal array can have an arbitrary number of dimensions, the only
+ limitation being that the last two dimensions must have the same length as
+ the axes arrays.
+
+ Sliders are provided to select indices on the first (n - 2) dimensions of
+ the signal array, and the plot is updated to show the image corresponding
+ to the selection.
+
+ If one or both of the axes does not have regularly spaced values, the
+ the image is plotted as a coloured scatter plot.
+ """
+ def __init__(self, parent=None, colormap=None):
+ """
+
+ :param parent: Parent QWidget
+ """
+ super(ArrayComplexImagePlot, self).__init__(parent)
+
+ self.__signals = None
+ self.__signals_names = None
+ self.__x_axis = None
+ self.__x_axis_name = None
+ self.__y_axis = None
+ self.__y_axis_name = None
+
+ self._plot = ComplexImageView(self)
+ if colormap is not None:
+ for mode in (ComplexImageView.ComplexMode.ABSOLUTE,
+ ComplexImageView.ComplexMode.SQUARE_AMPLITUDE,
+ ComplexImageView.ComplexMode.REAL,
+ ComplexImageView.ComplexMode.IMAGINARY):
+ self._plot.setColormap(colormap, mode)
+
+ self._plot.getPlot().getIntensityHistogramAction().setVisible(True)
+ self._plot.setKeepDataAspectRatio(True)
+
+ # not closable
+ self._selector = NumpyAxesSelector(self)
+ self._selector.setNamedAxesSelectorVisibility(False)
+ self._selector.selectionChanged.connect(self._updateImage)
+
+ self._auxSigSlider = HorizontalSliderWithBrowser(parent=self)
+ self._auxSigSlider.setMinimum(0)
+ self._auxSigSlider.setValue(0)
+ self._auxSigSlider.valueChanged[int].connect(self._sliderIdxChanged)
+ self._auxSigSlider.setToolTip("Select auxiliary signals")
+
+ layout = qt.QVBoxLayout()
+ layout.addWidget(self._plot)
+ layout.addWidget(self._selector)
+ layout.addWidget(self._auxSigSlider)
+
+ self.setLayout(layout)
+
+ def _sliderIdxChanged(self, value):
+ self._updateImage()
+
+ def getPlot(self):
+ """Returns the plot used for the display
+
+ :rtype: PlotWidget
+ """
+ return self._plot.getPlot()
+
+ def setImageData(self, signals,
+ x_axis=None, y_axis=None,
+ signals_names=None,
+ xlabel=None, ylabel=None,
+ title=None):
+ """
+
+ :param signals: list of n-D datasets, whose last 2 dimensions are used as the
+ image's values, or list of 3D datasets interpreted as RGBA image.
+ :param x_axis: 1-D dataset used as the image's x coordinates. If
+ provided, its lengths must be equal to the length of the last
+ dimension of ``signal``.
+ :param y_axis: 1-D dataset used as the image's y. If provided,
+ its lengths must be equal to the length of the 2nd to last
+ dimension of ``signal``.
+ :param signals_names: Names for each image, used as subtitle and legend.
+ :param xlabel: Label for X axis
+ :param ylabel: Label for Y axis
+ :param title: Graph title
+ """
+ self._selector.selectionChanged.disconnect(self._updateImage)
+ self._auxSigSlider.valueChanged.disconnect(self._sliderIdxChanged)
+
+ self.__signals = signals
+ self.__signals_names = signals_names
+ self.__x_axis = x_axis
+ self.__x_axis_name = xlabel
+ self.__y_axis = y_axis
+ self.__y_axis_name = ylabel
+ self.__title = title
+
+ self._selector.clear()
+ self._selector.setAxisNames(["Y", "X"])
+ self._selector.setData(signals[0])
+
+ if len(signals[0].shape) <= 2:
+ self._selector.hide()
+ else:
+ self._selector.show()
+
+ self._auxSigSlider.setMaximum(len(signals) - 1)
+ if len(signals) > 1:
+ self._auxSigSlider.show()
+ else:
+ self._auxSigSlider.hide()
+ self._auxSigSlider.setValue(0)
+
+ self._updateImage()
+ self._plot.getPlot().resetZoom()
+
+ self._selector.selectionChanged.connect(self._updateImage)
+ self._auxSigSlider.valueChanged.connect(self._sliderIdxChanged)
+
+ def _updateImage(self):
+ selection = self._selector.selection()
+ auxSigIdx = self._auxSigSlider.value()
+
+ images = [img[selection] for img in self.__signals]
+ image = images[auxSigIdx]
+
+ x_axis = self.__x_axis
+ y_axis = self.__y_axis
+
+ if x_axis is None and y_axis is None:
+ xcalib = NoCalibration()
+ ycalib = NoCalibration()
+ else:
+ if x_axis is None:
+ # no calibration
+ x_axis = numpy.arange(image.shape[1])
+ elif numpy.isscalar(x_axis) or len(x_axis) == 1:
+ # constant axis
+ x_axis = x_axis * numpy.ones((image.shape[1], ))
+ elif len(x_axis) == 2:
+ # linear calibration
+ x_axis = x_axis[0] * numpy.arange(image.shape[1]) + x_axis[1]
+
+ if y_axis is None:
+ y_axis = numpy.arange(image.shape[0])
+ elif numpy.isscalar(y_axis) or len(y_axis) == 1:
+ y_axis = y_axis * numpy.ones((image.shape[0], ))
+ elif len(y_axis) == 2:
+ y_axis = y_axis[0] * numpy.arange(image.shape[0]) + y_axis[1]
+
+ xcalib = ArrayCalibration(x_axis)
+ ycalib = ArrayCalibration(y_axis)
+
+ self._plot.setData(image)
+ if xcalib.is_affine():
+ xorigin, xscale = xcalib(0), xcalib.get_slope()
+ else:
+ _logger.warning("Unsupported complex image X axis calibration")
+ xorigin, xscale = 0., 1.
+
+ if ycalib.is_affine():
+ yorigin, yscale = ycalib(0), ycalib.get_slope()
+ else:
+ _logger.warning("Unsupported complex image Y axis calibration")
+ yorigin, yscale = 0., 1.
+
+ self._plot.setOrigin((xorigin, yorigin))
+ self._plot.setScale((xscale, yscale))
+
+ title = ""
+ if self.__title:
+ title += self.__title
+ if not title.strip().endswith(self.__signals_names[auxSigIdx]):
+ title += "\n" + self.__signals_names[auxSigIdx]
+ self._plot.setGraphTitle(title)
+ self._plot.getXAxis().setLabel(self.__x_axis_name)
+ self._plot.getYAxis().setLabel(self.__y_axis_name)
+
+ def clear(self):
+ old = self._selector.blockSignals(True)
+ self._selector.clear()
+ self._selector.blockSignals(old)
+ self._plot.setData(None)
+
+
class ArrayStackPlot(qt.QWidget):
"""
Widget for plotting a n-D array (n >= 3) as a stack of images.
@@ -665,4 +855,171 @@ class ArrayStackPlot(qt.QWidget):
self.__x_axis_name])
def clear(self):
+ old = self._selector.blockSignals(True)
+ self._selector.clear()
+ self._selector.blockSignals(old)
self._stack_view.clear()
+
+
+class ArrayVolumePlot(qt.QWidget):
+ """
+ Widget for plotting a n-D array (n >= 3) as a 3D scalar field.
+ Three axis arrays can be provided to calibrate the axes.
+
+ The signal array can have an arbitrary number of dimensions, the only
+ limitation being that the last 3 dimensions must have the same length as
+ the axes arrays.
+
+ Sliders are provided to select indices on the first (n - 3) dimensions of
+ the signal array, and the plot is updated to load the stack corresponding
+ to the selection.
+ """
+ def __init__(self, parent=None):
+ """
+
+ :param parent: Parent QWidget
+ """
+ super(ArrayVolumePlot, self).__init__(parent)
+
+ self.__signal = None
+ self.__signal_name = None
+ # the Z, Y, X axes apply to the last three dimensions of the signal
+ # (in that order)
+ self.__z_axis = None
+ self.__z_axis_name = None
+ self.__y_axis = None
+ self.__y_axis_name = None
+ self.__x_axis = None
+ self.__x_axis_name = None
+
+ from ._VolumeWindow import VolumeWindow
+
+ self._view = VolumeWindow(self)
+
+ self._hline = qt.QFrame(self)
+ self._hline.setFrameStyle(qt.QFrame.HLine)
+ self._hline.setFrameShadow(qt.QFrame.Sunken)
+ self._legend = qt.QLabel(self)
+ self._selector = NumpyAxesSelector(self)
+ self._selector.setNamedAxesSelectorVisibility(False)
+ self.__selector_is_connected = False
+
+ layout = qt.QVBoxLayout()
+ layout.addWidget(self._view)
+ layout.addWidget(self._hline)
+ layout.addWidget(self._legend)
+ layout.addWidget(self._selector)
+
+ self.setLayout(layout)
+
+ def getVolumeView(self):
+ """Returns the plot used for the display
+
+ :rtype: SceneWindow
+ """
+ return self._view
+
+ def setData(self, signal,
+ x_axis=None, y_axis=None, z_axis=None,
+ signal_name=None,
+ xlabel=None, ylabel=None, zlabel=None,
+ title=None):
+ """
+
+ :param signal: n-D dataset, whose last 3 dimensions are used as the
+ 3D stack values.
+ :param x_axis: 1-D dataset used as the image's x coordinates. If
+ provided, its lengths must be equal to the length of the last
+ dimension of ``signal``.
+ :param y_axis: 1-D dataset used as the image's y. If provided,
+ its lengths must be equal to the length of the 2nd to last
+ dimension of ``signal``.
+ :param z_axis: 1-D dataset used as the image's z. If provided,
+ its lengths must be equal to the length of the 3rd to last
+ dimension of ``signal``.
+ :param signal_name: Label used in the legend
+ :param xlabel: Label for X axis
+ :param ylabel: Label for Y axis
+ :param zlabel: Label for Z axis
+ :param title: Graph title
+ """
+ if self.__selector_is_connected:
+ self._selector.selectionChanged.disconnect(self._updateVolume)
+ self.__selector_is_connected = False
+
+ self.__signal = signal
+ self.__signal_name = signal_name or ""
+ self.__x_axis = x_axis
+ self.__x_axis_name = xlabel
+ self.__y_axis = y_axis
+ self.__y_axis_name = ylabel
+ self.__z_axis = z_axis
+ self.__z_axis_name = zlabel
+
+ self._selector.setData(signal)
+ self._selector.setAxisNames(["Y", "X", "Z"])
+
+ self._updateVolume()
+
+ # the legend label shows the selection slice producing the volume
+ # (only interesting for ndim > 3)
+ if signal.ndim > 3:
+ self._selector.setVisible(True)
+ self._legend.setVisible(True)
+ self._hline.setVisible(True)
+ else:
+ self._selector.setVisible(False)
+ self._legend.setVisible(False)
+ self._hline.setVisible(False)
+
+ if not self.__selector_is_connected:
+ self._selector.selectionChanged.connect(self._updateVolume)
+ self.__selector_is_connected = True
+
+ def _updateVolume(self):
+ """Update displayed stack according to the current axes selector
+ data."""
+ x_axis = self.__x_axis
+ y_axis = self.__y_axis
+ z_axis = self.__z_axis
+
+ offset = []
+ scale = []
+ for axis in [x_axis, y_axis, z_axis]:
+ if axis is None:
+ calibration = NoCalibration()
+ elif len(axis) == 2:
+ calibration = LinearCalibration(
+ y_intercept=axis[0], slope=axis[1])
+ else:
+ calibration = ArrayCalibration(axis)
+ if not calibration.is_affine():
+ _logger.warning("Axis has not linear values, ignored")
+ offset.append(0.)
+ scale.append(1.)
+ else:
+ offset.append(calibration(0))
+ scale.append(calibration.get_slope())
+
+ legend = self.__signal_name + "["
+ for sl in self._selector.selection():
+ if sl == slice(None):
+ legend += ":, "
+ else:
+ legend += str(sl) + ", "
+ legend = legend[:-2] + "]"
+ self._legend.setText("Displayed data: " + legend)
+
+ # Update SceneWidget
+ data = self._selector.selectedData()
+
+ volumeView = self.getVolumeView()
+ volumeView.setData(data, offset=offset, scale=scale)
+ volumeView.setAxesLabels(
+ self.__x_axis_name, self.__y_axis_name, self.__z_axis_name)
+
+ def clear(self):
+ old = self._selector.blockSignals(True)
+ self._selector.clear()
+ self._selector.blockSignals(old)
+ self.getVolumeView().clear()
diff --git a/silx/gui/data/TextFormatter.py b/silx/gui/data/TextFormatter.py
index 1401634..98c37d7 100644
--- a/silx/gui/data/TextFormatter.py
+++ b/silx/gui/data/TextFormatter.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,16 +29,15 @@ __authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "24/07/2018"
-import numpy
+import logging
import numbers
-from silx.third_party import six
+
+import numpy
+import six
+
from silx.gui import qt
-import logging
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
_logger = logging.getLogger(__name__)
@@ -322,10 +321,9 @@ class TextFormatter(qt.QObject):
if dtype.kind == 'S':
return self.__formatCharString(data)
elif dtype.kind == 'O':
- if h5py is not None:
- text = self.__formatH5pyObject(data, dtype)
- if text is not None:
- return text
+ text = self.__formatH5pyObject(data, dtype)
+ if text is not None:
+ return text
try:
# Try ascii/utf-8
text = "%s" % data.decode("utf-8")
@@ -339,15 +337,14 @@ class TextFormatter(qt.QObject):
elif isinstance(data, (numpy.integer)):
if dtype is None:
dtype = data.dtype
- if h5py is not None:
- enumType = h5py.check_dtype(enum=dtype)
- if enumType is not None:
- for key, value in enumType.items():
- if value == data:
- result = {}
- result["name"] = key
- result["value"] = data
- return self.__enumFormat % result
+ enumType = h5py.check_dtype(enum=dtype)
+ if enumType is not None:
+ for key, value in enumType.items():
+ if value == data:
+ result = {}
+ result["name"] = key
+ result["value"] = data
+ return self.__enumFormat % result
return self.__integerFormat % data
elif isinstance(data, (numbers.Integral)):
return self.__integerFormat % data
@@ -373,21 +370,20 @@ class TextFormatter(qt.QObject):
template = self.__floatFormat
params = (data.real)
return template % params
- elif h5py is not None and isinstance(data, h5py.h5r.Reference):
+ elif isinstance(data, h5py.h5r.Reference):
dtype = h5py.special_dtype(ref=h5py.Reference)
text = self.__formatH5pyObject(data, dtype)
return text
- elif h5py is not None and isinstance(data, h5py.h5r.RegionReference):
+ elif isinstance(data, h5py.h5r.RegionReference):
dtype = h5py.special_dtype(ref=h5py.RegionReference)
text = self.__formatH5pyObject(data, dtype)
return text
elif isinstance(data, numpy.object_) or dtype is not None:
if dtype is None:
dtype = data.dtype
- if h5py is not None:
- text = self.__formatH5pyObject(data, dtype)
- if text is not None:
- return text
+ text = self.__formatH5pyObject(data, dtype)
+ if text is not None:
+ return text
# That's a numpy object
return str(data)
return str(data)
diff --git a/silx/gui/data/_VolumeWindow.py b/silx/gui/data/_VolumeWindow.py
new file mode 100644
index 0000000..03b6876
--- /dev/null
+++ b/silx/gui/data/_VolumeWindow.py
@@ -0,0 +1,148 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This module provides a widget to visualize 3D arrays"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "22/03/2019"
+
+
+import numpy
+
+from .. import qt
+from ..plot3d.SceneWindow import SceneWindow
+from ..plot3d.items import ScalarField3D, ComplexField3D, ItemChangedType
+
+
+class VolumeWindow(SceneWindow):
+ """Extends SceneWindow with a convenient API for 3D array
+
+ :param QWidget: parent
+ """
+
+ def __init__(self, parent):
+ super(VolumeWindow, self).__init__(parent)
+ self.__firstData = True
+ # Hide global parameter dock
+ self.getGroupResetWidget().parent().setVisible(False)
+
+ def setAxesLabels(self, xlabel=None, ylabel=None, zlabel=None):
+ """Set the text labels of the axes.
+
+ :param Union[str,None] xlabel: Label of the X axis
+ :param Union[str,None] ylabel: Label of the Y axis
+ :param Union[str,None] zlabel: Label of the Z axis
+ """
+ sceneWidget = self.getSceneWidget()
+ sceneWidget.getSceneGroup().setAxesLabels(
+ 'X' if xlabel is None else xlabel,
+ 'Y' if ylabel is None else ylabel,
+ 'Z' if zlabel is None else zlabel)
+
+ def clear(self):
+ """Clear any currently displayed data"""
+ sceneWidget = self.getSceneWidget()
+ items = sceneWidget.getItems()
+ if (len(items) == 1 and
+ isinstance(items[0], (ScalarField3D, ComplexField3D))):
+ items[0].setData(None)
+ else: # Safety net
+ sceneWidget.clearItems()
+
+ @staticmethod
+ def __computeIsolevel(data):
+ """Returns a suitable isolevel value for data
+
+ :param numpy.ndarray data:
+ :rtype: float
+ """
+ data = data[numpy.isfinite(data)]
+ if len(data) == 0:
+ return 0
+ else:
+ return numpy.mean(data) + numpy.std(data)
+
+ def setData(self, data, offset=(0., 0., 0.), scale=(1., 1., 1.)):
+ """Set the 3D array data to display.
+
+ :param numpy.ndarray data: 3D array of float or complex
+ :param List[float] offset: (tx, ty, tz) coordinates of the origin
+ :param List[float] scale: (sx, sy, sz) scale for each dimension
+ """
+ sceneWidget = self.getSceneWidget()
+ dataMaxCoords = numpy.array(list(reversed(data.shape))) - 1
+
+ previousItems = sceneWidget.getItems()
+ if (len(previousItems) == 1 and
+ isinstance(previousItems[0], (ScalarField3D, ComplexField3D)) and
+ numpy.iscomplexobj(data) == isinstance(previousItems[0], ComplexField3D)):
+ # Reuse existing volume item
+ volume = sceneWidget.getItems()[0]
+ volume.setData(data, copy=False)
+ # Make sure the plane goes through the dataset
+ for plane in volume.getCutPlanes():
+ point = numpy.array(plane.getPoint())
+ if numpy.any(point < (0, 0, 0)) or numpy.any(point > dataMaxCoords):
+ plane.setPoint(dataMaxCoords // 2)
+ else:
+ # Add a new volume
+ sceneWidget.clearItems()
+ volume = sceneWidget.addVolume(data, copy=False)
+ volume.setLabel('Volume')
+ for plane in volume.getCutPlanes():
+ # Make plane going through the center of the data
+ plane.setPoint(dataMaxCoords // 2)
+ plane.setVisible(False)
+ plane.sigItemChanged.connect(self.__cutPlaneUpdated)
+ volume.addIsosurface(self.__computeIsolevel, '#FF0000FF')
+
+ # Expand the parameter tree
+ model = self.getParamTreeView().model()
+ index = qt.QModelIndex() # Invalid index for top level
+ while 1:
+ rowCount = model.rowCount(parent=index)
+ if rowCount == 0:
+ break
+ index = model.index(rowCount - 1, 0, parent=index)
+ self.getParamTreeView().setExpanded(index, True)
+ if not index.isValid():
+ break
+
+ volume.setTranslation(*offset)
+ volume.setScale(*scale)
+
+ if self.__firstData: # Only center for first dataset
+ self.__firstData = False
+ sceneWidget.centerScene()
+
+ def __cutPlaneUpdated(self, event):
+ """Handle the change of visibility of the cut plane
+
+ :param event: Kind of update
+ """
+ if event == ItemChangedType.VISIBLE:
+ plane = self.sender()
+ if plane.isVisible():
+ self.getSceneWidget().selection().setCurrentItem(plane)
diff --git a/silx/gui/data/test/test_arraywidget.py b/silx/gui/data/test/test_arraywidget.py
index 50ffc84..6bcbbd3 100644
--- a/silx/gui/data/test/test_arraywidget.py
+++ b/silx/gui/data/test/test_arraywidget.py
@@ -36,10 +36,7 @@ from silx.gui import qt
from silx.gui.data import ArrayTableWidget
from silx.gui.utils.testutils import TestCaseQt
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
class TestArrayWidget(TestCaseQt):
@@ -190,7 +187,6 @@ class TestArrayWidget(TestCaseQt):
self.assertIs(b0, b1)
-@unittest.skipIf(h5py is None, "Could not import h5py")
class TestH5pyArrayWidget(TestCaseQt):
"""Basic test for ArrayTableWidget with a dataset.
diff --git a/silx/gui/data/test/test_dataviewer.py b/silx/gui/data/test/test_dataviewer.py
index a681f33..12a640e 100644
--- a/silx/gui/data/test/test_dataviewer.py
+++ b/silx/gui/data/test/test_dataviewer.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -24,7 +24,7 @@
# ###########################################################################*/
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "23/04/2018"
+__date__ = "19/02/2019"
import os
import tempfile
@@ -42,10 +42,7 @@ from silx.gui.data.DataViewerFrame import DataViewerFrame
from silx.gui.utils.testutils import SignalListener
from silx.gui.utils.testutils import TestCaseQt
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
class _DataViewMock(DataView):
@@ -170,8 +167,6 @@ class AbstractDataViewerTests(TestCaseQt):
self.assertEqual(DataViews.RAW_MODE, widget.displayedView().modeId())
def test_3d_h5_dataset(self):
- if h5py is None:
- self.skipTest("h5py library is not available")
with self.h5_temporary_file() as h5file:
dataset = h5file["data"]
widget = self.create_widget()
@@ -242,12 +237,13 @@ class AbstractDataViewerTests(TestCaseQt):
# replace a view that is a child of a composite view
widget = self.create_widget()
view = _DataViewMock(widget)
- widget.replaceView(DataViews.NXDATA_INVALID_MODE,
- view)
+ replaced = widget.replaceView(DataViews.NXDATA_INVALID_MODE,
+ view)
+ self.assertTrue(replaced)
nxdata_view = widget.getViewFromModeId(DataViews.NXDATA_MODE)
self.assertNotIn(DataViews.NXDATA_INVALID_MODE,
- [v.modeId() for v in nxdata_view.availableViews()])
- self.assertTrue(view in nxdata_view.availableViews())
+ [v.modeId() for v in nxdata_view.getViews()])
+ self.assertTrue(view in nxdata_view.getViews())
class TestDataViewer(AbstractDataViewerTests):
diff --git a/silx/gui/data/test/test_numpyaxesselector.py b/silx/gui/data/test/test_numpyaxesselector.py
index 6b7b58c..df11c1a 100644
--- a/silx/gui/data/test/test_numpyaxesselector.py
+++ b/silx/gui/data/test/test_numpyaxesselector.py
@@ -37,10 +37,7 @@ from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector
from silx.gui.utils.testutils import SignalListener
from silx.gui.utils.testutils import TestCaseQt
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
class TestNumpyAxesSelector(TestCaseQt):
@@ -121,8 +118,6 @@ class TestNumpyAxesSelector(TestCaseQt):
os.unlink(tmp_name)
def test_h5py_dataset(self):
- if h5py is None:
- self.skipTest("h5py library is not available")
with self.h5_temporary_file() as h5file:
dataset = h5file["data"]
expectedResult = dataset[0]
diff --git a/silx/gui/data/test/test_textformatter.py b/silx/gui/data/test/test_textformatter.py
index 850aa00..1a63074 100644
--- a/silx/gui/data/test/test_textformatter.py
+++ b/silx/gui/data/test/test_textformatter.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,17 +29,15 @@ __date__ = "12/12/2017"
import unittest
import shutil
import tempfile
+
import numpy
+import six
from silx.gui.utils.testutils import TestCaseQt
from silx.gui.utils.testutils import SignalListener
from ..TextFormatter import TextFormatter
-from silx.third_party import six
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
class TestTextFormatter(TestCaseQt):
@@ -50,7 +48,7 @@ class TestTextFormatter(TestCaseQt):
self.assertIsNot(formatter, copy)
copy.setFloatFormat("%.3f")
self.assertEqual(formatter.integerFormat(), copy.integerFormat())
- self.assertNotEquals(formatter.floatFormat(), copy.floatFormat())
+ self.assertNotEqual(formatter.floatFormat(), copy.floatFormat())
self.assertEqual(formatter.useQuoteForText(), copy.useQuoteForText())
self.assertEqual(formatter.imaginaryUnit(), copy.imaginaryUnit())
@@ -108,8 +106,6 @@ class TestTextFormatterWithH5py(TestCaseQt):
@classmethod
def setUpClass(cls):
super(TestTextFormatterWithH5py, cls).setUpClass()
- if h5py is None:
- raise unittest.SkipTest("h5py is not available")
cls.tmpDirectory = tempfile.mkdtemp()
cls.h5File = h5py.File("%s/formatter.h5" % cls.tmpDirectory, mode="w")
diff --git a/silx/gui/dialog/AbstractDataFileDialog.py b/silx/gui/dialog/AbstractDataFileDialog.py
index 40045fe..29e7bb5 100644
--- a/silx/gui/dialog/AbstractDataFileDialog.py
+++ b/silx/gui/dialog/AbstractDataFileDialog.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -28,29 +28,36 @@ This module contains an :class:`AbstractDataFileDialog`.
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "05/03/2018"
+__date__ = "05/03/2019"
import sys
import os
import logging
-import numpy
import functools
+from distutils.version import LooseVersion
+
+import numpy
+import six
+
import silx.io.url
from silx.gui import qt
from silx.gui.hdf5.Hdf5TreeModel import Hdf5TreeModel
from . import utils
-from silx.third_party import six
from .FileTypeComboBox import FileTypeComboBox
-try:
- import fabio
-except ImportError:
- fabio = None
+
+import fabio
_logger = logging.getLogger(__name__)
+DEFAULT_SIDEBAR_URL = True
+"""Set it to false to disable initilializing of the sidebar urls with the
+default Qt list. This could allow to disable a behaviour known to segfault on
+some version of PyQt."""
+
+
class _IconProvider(object):
FileDialogToParentDir = qt.QStyle.SP_CustomBase + 1
@@ -143,14 +150,22 @@ class _SideBar(qt.QListView):
:rtype: List[str]
"""
urls = []
- if qt.qVersion().startswith("5.") and sys.platform in ["linux", "linux2"]:
+ version = LooseVersion(qt.qVersion())
+ feed_sidebar = True
+
+ if not DEFAULT_SIDEBAR_URL:
+ _logger.debug("Skip default sidebar URLs (from setted variable)")
+ feed_sidebar = False
+ elif version.version[0] == 4 and sys.platform in ["win32"]:
+ # Avoid locking the GUI 5min in case of use of network driver
+ _logger.debug("Skip default sidebar URLs (avoid lock when using network drivers)")
+ feed_sidebar = False
+ elif version < LooseVersion("5.11.2") and qt.BINDING == "PyQt5" and sys.platform in ["linux", "linux2"]:
# Avoid segfault on PyQt5 + gtk
_logger.debug("Skip default sidebar URLs (avoid PyQt5 segfault)")
- pass
- elif qt.qVersion().startswith("4.") and sys.platform in ["win32"]:
- # Avoid 5min of locked GUI relative to network driver
- _logger.debug("Skip default sidebar URLs (avoid lock when using network drivers)")
- else:
+ feed_sidebar = False
+
+ if feed_sidebar:
# Get default shortcut
# There is no other way
d = qt.QFileDialog(self)
@@ -453,9 +468,13 @@ class _FabioData(object):
def shape(self):
if self.__fabioFile.nframes == 0:
return None
+ if self.__fabioFile.nframes == 1:
+ return [slice(None), slice(None)]
return [self.__fabioFile.nframes, slice(None), slice(None)]
def __getitem__(self, selector):
+ if self.__fabioFile.nframes == 1 and selector == tuple():
+ return self.__fabioFile.data
if isinstance(selector, tuple) and len(selector) == 1:
selector = selector[0]
@@ -527,6 +546,10 @@ class AbstractDataFileDialog(qt.QDialog):
def _init(self):
self.setWindowTitle("Open")
+ self.__openedFiles = []
+ """Store the list of files opened by the model itself."""
+ # FIXME: It should be managed one by one by Hdf5Item itself
+
self.__directory = None
self.__directoryLoadedFilter = None
self.__errorWhileLoadingFile = None
@@ -576,10 +599,6 @@ class AbstractDataFileDialog(qt.QDialog):
self.__fileTypeCombo.setCurrentIndex(0)
self.__filterSelected(0)
- self.__openedFiles = []
- """Store the list of files opened by the model itself."""
- # FIXME: It should be managed one by one by Hdf5Item itself
-
# It is not possible to override the QObject destructor nor
# to access to the content of the Python object with the `destroyed`
# signal cause the Python method was already removed with the QWidget,
@@ -1023,15 +1042,16 @@ class AbstractDataFileDialog(qt.QDialog):
return
self.__directoryLoadedFilter = path
self.__processing += 1
+ if self.__fileModel is None:
+ return
index = self.__fileModel.setRootPath(path)
if not index.isValid():
+ # There is a problem with this path
+ # No asynchronous process will be waked up
self.__processing -= 1
self.__browser.setRootIndex(index, model=self.__fileModel)
self.__clearData()
self.__updatePath()
- else:
- # asynchronous process
- pass
def __directoryLoaded(self, path):
if self.__directoryLoadedFilter is not None:
@@ -1040,6 +1060,8 @@ class AbstractDataFileDialog(qt.QDialog):
# The first click on the sidebar sent 2 events
self.__processing -= 1
return
+ if self.__fileModel is None:
+ return
index = self.__fileModel.index(path)
self.__browser.setRootIndex(index, model=self.__fileModel)
self.__updatePath()
@@ -1061,8 +1083,6 @@ class AbstractDataFileDialog(qt.QDialog):
def __openFabioFile(self, filename):
self.__closeFile()
try:
- if fabio is None:
- raise ImportError("Fabio module is not available")
self.__fabio = fabio.open(filename)
self.__openedFiles.append(self.__fabio)
self.__selectedFile = filename
@@ -1108,10 +1128,10 @@ class AbstractDataFileDialog(qt.QDialog):
if codec.is_autodetect():
if self.__isSilxHavePriority(filename):
openners.append(self.__openSilxFile)
- if fabio is not None and self._isFabioFilesSupported():
+ if self._isFabioFilesSupported():
openners.append(self.__openFabioFile)
else:
- if fabio is not None and self._isFabioFilesSupported():
+ if self._isFabioFilesSupported():
openners.append(self.__openFabioFile)
openners.append(self.__openSilxFile)
elif codec.is_silx_codec():
@@ -1159,10 +1179,9 @@ class AbstractDataFileDialog(qt.QDialog):
is_fabio_have_priority = not codec.is_silx_codec() and not self.__isSilxHavePriority(path)
if is_fabio_decoder or is_fabio_have_priority:
# Then it's flat frame container
- if fabio is not None:
- self.__openFabioFile(path)
- if self.__fabio is not None:
- selectedData = _FabioData(self.__fabio)
+ self.__openFabioFile(path)
+ if self.__fabio is not None:
+ selectedData = _FabioData(self.__fabio)
else:
assert(False)
@@ -1221,6 +1240,7 @@ class AbstractDataFileDialog(qt.QDialog):
if self.__previewWidget is not None:
self.__previewWidget.setData(None)
if self.__selectorWidget is not None:
+ self.__selectorWidget.setData(None)
self.__selectorWidget.hide()
self.__selectedData = None
self.__data = None
@@ -1238,6 +1258,8 @@ class AbstractDataFileDialog(qt.QDialog):
If :meth:`_isDataSupported` returns false, this function will be
inhibited and no data will be selected.
"""
+ if isinstance(data, _FabioData):
+ data = data[()]
if self.__previewWidget is not None:
fromDataSelector = self.__selectedData is not None
self.__previewWidget.setData(data, fromDataSelector=fromDataSelector)
@@ -1305,8 +1327,10 @@ class AbstractDataFileDialog(qt.QDialog):
filename = ""
dataPath = None
- if useSelectorWidget and self.__selectorWidget is not None and self.__selectorWidget.isVisible():
+ if useSelectorWidget and self.__selectorWidget is not None and self.__selectorWidget.isUsed():
slicing = self.__selectorWidget.slicing()
+ if slicing == tuple():
+ slicing = None
else:
slicing = None
@@ -1471,9 +1495,7 @@ class AbstractDataFileDialog(qt.QDialog):
self.__clearData()
if self.__selectorWidget is not None:
- self.__selectorWidget.setVisible(url.data_slice() is not None)
- if url.data_slice() is not None:
- self.__selectorWidget.setSlicing(url.data_slice())
+ self.__selectorWidget.selectSlicing(url.data_slice())
else:
self.__errorWhileLoadingFile = (url.file_path(), "File not found")
self.__clearData()
diff --git a/silx/gui/dialog/ColormapDialog.py b/silx/gui/dialog/ColormapDialog.py
index cbbfa5a..9c956f8 100644
--- a/silx/gui/dialog/ColormapDialog.py
+++ b/silx/gui/dialog/ColormapDialog.py
@@ -63,9 +63,10 @@ from __future__ import division
__authors__ = ["V.A. Sole", "T. Vincent", "H. Payno"]
__license__ = "MIT"
-__date__ = "23/05/2018"
+__date__ = "27/11/2018"
+import enum
import logging
import numpy
@@ -73,10 +74,10 @@ import numpy
from .. import qt
from ..colors import Colormap, preferredColormaps
from ..plot import PlotWidget
+from ..plot.items.axis import Axis
from silx.gui.widgets.FloatEdit import FloatEdit
import weakref
from silx.math.combo import min_max
-from silx.third_party import enum
from silx.gui import icons
from silx.math.histogram import Histogramnd
@@ -154,39 +155,59 @@ class _ColormapNameCombox(qt.QComboBox):
qt.QComboBox.__init__(self, parent)
self.__initItems()
- ORIGINAL_NAME = qt.Qt.UserRole + 1
+ LUT_NAME = qt.Qt.UserRole + 1
+ LUT_COLORS = qt.Qt.UserRole + 2
def __initItems(self):
for colormapName in preferredColormaps():
index = self.count()
self.addItem(str.title(colormapName))
- self.setItemIcon(index, self.getIconPreview(colormapName))
- self.setItemData(index, colormapName, role=self.ORIGINAL_NAME)
+ self.setItemIcon(index, self.getIconPreview(name=colormapName))
+ self.setItemData(index, colormapName, role=self.LUT_NAME)
- def getIconPreview(self, colormapName):
+ def getIconPreview(self, name=None, colors=None):
"""Return an icon preview from a LUT name.
This icons are cached into a global structure.
- :param str colormapName: str
+ :param str name: Name of the LUT
+ :param numpy.ndarray colors: Colors identify the LUT
:rtype: qt.QIcon
"""
- if colormapName not in _colormapIconPreview:
- icon = self.createIconPreview(colormapName)
- _colormapIconPreview[colormapName] = icon
- return _colormapIconPreview[colormapName]
-
- def createIconPreview(self, colormapName):
+ if name is not None:
+ iconKey = name
+ else:
+ iconKey = tuple(colors)
+ icon = _colormapIconPreview.get(iconKey, None)
+ if icon is None:
+ icon = self.createIconPreview(name, colors)
+ _colormapIconPreview[iconKey] = icon
+ return icon
+
+ def createIconPreview(self, name=None, colors=None):
"""Create and return an icon preview from a LUT name.
This icons are cached into a global structure.
- :param str colormapName: Name of the LUT
+ :param str name: Name of the LUT
+ :param numpy.ndarray colors: Colors identify the LUT
:rtype: qt.QIcon
"""
- colormap = Colormap(colormapName)
+ colormap = Colormap(name)
size = 32
- lut = colormap.getNColors(size)
+ if name is not None:
+ lut = colormap.getNColors(size)
+ else:
+ lut = colors
+ if len(lut) > size:
+ # Down sample
+ step = int(len(lut) / size)
+ lut = lut[::step]
+ elif len(lut) < size:
+ # Over sample
+ indexes = numpy.arange(size) / float(size) * (len(lut) - 1)
+ indexes = indexes.astype("int")
+ lut = lut[indexes]
if lut is None or len(lut) == 0:
return qt.QIcon()
@@ -204,18 +225,50 @@ class _ColormapNameCombox(qt.QComboBox):
return qt.QIcon(pixmap)
def getCurrentName(self):
- return self.itemData(self.currentIndex(), self.ORIGINAL_NAME)
+ return self.itemData(self.currentIndex(), self.LUT_NAME)
+
+ def getCurrentColors(self):
+ return self.itemData(self.currentIndex(), self.LUT_COLORS)
+
+ def findLutName(self, name):
+ return self.findData(name, role=self.LUT_NAME)
+
+ def findLutColors(self, lut):
+ for index in range(self.count()):
+ if self.itemData(index, role=self.LUT_NAME) is not None:
+ continue
+ colors = self.itemData(index, role=self.LUT_COLORS)
+ if colors is None:
+ continue
+ if numpy.array_equal(colors, lut):
+ return index
+ return -1
+
+ def setCurrentLut(self, colormap):
+ name = colormap.getName()
+ if name is not None:
+ self._setCurrentName(name)
+ else:
+ lut = colormap.getColormapLUT()
+ self._setCurrentLut(lut)
- def findColormap(self, name):
- return self.findData(name, role=self.ORIGINAL_NAME)
+ def _setCurrentLut(self, lut):
+ index = self.findLutColors(lut)
+ if index == -1:
+ index = self.count()
+ self.addItem("Custom")
+ self.setItemIcon(index, self.getIconPreview(colors=lut))
+ self.setItemData(index, None, role=self.LUT_NAME)
+ self.setItemData(index, lut, role=self.LUT_COLORS)
+ self.setCurrentIndex(index)
- def setCurrentName(self, name):
- index = self.findColormap(name)
+ def _setCurrentName(self, name):
+ index = self.findLutName(name)
if index < 0:
index = self.count()
self.addItem(str.title(name))
- self.setItemIcon(index, self.getIconPreview(name))
- self.setItemData(index, name, role=self.ORIGINAL_NAME)
+ self.setItemIcon(index, self.getIconPreview(name=name))
+ self.setItemData(index, name, role=self.LUT_NAME)
self.setCurrentIndex(index)
@@ -255,6 +308,7 @@ class ColormapDialog(qt.QDialog):
the self.setcolormap is a callback)
"""
+ self.__displayInvalidated = False
self._histogramData = None
self._minMaxWasEdited = False
self._initialRange = None
@@ -276,20 +330,19 @@ class ColormapDialog(qt.QDialog):
# Colormap row
self._comboBoxColormap = _ColormapNameCombox(parent=formWidget)
- self._comboBoxColormap.currentIndexChanged[int].connect(self._updateName)
+ self._comboBoxColormap.currentIndexChanged[int].connect(self._updateLut)
formLayout.addRow('Colormap:', self._comboBoxColormap)
# Normalization row
self._normButtonLinear = qt.QRadioButton('Linear')
self._normButtonLinear.setChecked(True)
self._normButtonLog = qt.QRadioButton('Log')
- self._normButtonLog.toggled.connect(self._activeLogNorm)
normButtonGroup = qt.QButtonGroup(self)
normButtonGroup.setExclusive(True)
normButtonGroup.addButton(self._normButtonLinear)
normButtonGroup.addButton(self._normButtonLog)
- self._normButtonLinear.toggled[bool].connect(self._updateLinearNorm)
+ normButtonGroup.buttonClicked[qt.QAbstractButton].connect(self._updateNormalization)
normLayout = qt.QHBoxLayout()
normLayout.setContentsMargins(0, 0, 0, 0)
@@ -388,9 +441,17 @@ class ColormapDialog(qt.QDialog):
self.setFixedSize(self.sizeHint())
self._applyColormap()
+ def _displayLater(self):
+ self.__displayInvalidated = True
+
def showEvent(self, event):
self.visibleChanged.emit(True)
super(ColormapDialog, self).showEvent(event)
+ if self.isVisible():
+ if self.__displayInvalidated:
+ self._applyColormap()
+ self._updateDataInPlot()
+ self.__displayInvalidated = False
def closeEvent(self, event):
if not self.isModal():
@@ -434,6 +495,54 @@ class ColormapDialog(qt.QDialog):
def sizeHint(self):
return self.layout().minimumSize()
+ def _computeView(self, dataMin, dataMax):
+ """Compute the location of the view according to the bound of the data
+
+ :rtype: Tuple(float, float)
+ """
+ marginRatio = 1.0 / 6.0
+ scale = self._plot.getXAxis().getScale()
+
+ if self._dataRange is not None:
+ if scale == Axis.LOGARITHMIC:
+ minRange = self._dataRange[1]
+ else:
+ minRange = self._dataRange[0]
+ maxRange = self._dataRange[2]
+ if minRange is not None:
+ dataMin = min(dataMin, minRange)
+ dataMax = max(dataMax, maxRange)
+
+ if self._histogramData is not None:
+ info = min_max(self._histogramData[1])
+ if scale == Axis.LOGARITHMIC:
+ minHisto = info.min_positive
+ else:
+ minHisto = info.minimum
+ maxHisto = info.maximum
+ if minHisto is not None:
+ dataMin = min(dataMin, minHisto)
+ dataMax = max(dataMax, maxHisto)
+
+ if scale == Axis.LOGARITHMIC:
+ epsilon = numpy.finfo(numpy.float32).eps
+ if dataMin == 0:
+ dataMin = epsilon
+ if dataMax < dataMin:
+ dataMax = dataMin + epsilon
+ marge = marginRatio * abs(numpy.log10(dataMax) - numpy.log10(dataMin))
+ viewMin = 10**(numpy.log10(dataMin) - marge)
+ viewMax = 10**(numpy.log10(dataMax) + marge)
+ else: # scale == Axis.LINEAR:
+ marge = marginRatio * abs(dataMax - dataMin)
+ if marge < 0.0001:
+ # Smaller that the QLineEdit precision
+ marge = 0.0001
+ viewMin = dataMin - marge
+ viewMax = dataMax + marge
+
+ return viewMin, viewMax
+
def _plotUpdate(self, updateMarkers=True):
"""Update the plot content
@@ -454,27 +563,8 @@ class ColormapDialog(qt.QDialog):
if minData > maxData:
# avoid a full collapse
minData, maxData = maxData, minData
- minimum = minData
- maximum = maxData
-
- if self._dataRange is not None:
- minRange = self._dataRange[0]
- maxRange = self._dataRange[2]
- minimum = min(minimum, minRange)
- maximum = max(maximum, maxRange)
- if self._histogramData is not None:
- minHisto = self._histogramData[1][0]
- maxHisto = self._histogramData[1][-1]
- minimum = min(minimum, minHisto)
- maximum = max(maximum, maxHisto)
-
- marge = abs(maximum - minimum) / 6.0
- if marge < 0.0001:
- # Smaller that the QLineEdit precision
- marge = 0.0001
-
- minView, maxView = minimum - marge, maximum + marge
+ minView, maxView = self._computeView(minData, maxData)
if updateMarkers:
# Save the state in we are not moving the markers
@@ -483,6 +573,9 @@ class ColormapDialog(qt.QDialog):
minView = min(minView, self._initialRange[0])
maxView = max(maxView, self._initialRange[1])
+ if minView > minData:
+ # Hide the min range
+ minData = minView
x = [minView, minData, maxData, maxView]
y = [0, 0, 1, 1]
@@ -493,26 +586,37 @@ class ColormapDialog(qt.QDialog):
linestyle='-',
resetzoom=False)
+ scale = self._plot.getXAxis().getScale()
+
if updateMarkers:
- minDraggable = (self._colormap().isEditable() and
- not self._minValue.isAutoChecked())
- self._plot.addXMarker(
- self._minValue.getFiniteValue(),
- legend='Min',
- text='Min',
- draggable=minDraggable,
- color='blue',
- constraint=self._plotMinMarkerConstraint)
-
- maxDraggable = (self._colormap().isEditable() and
- not self._maxValue.isAutoChecked())
- self._plot.addXMarker(
- self._maxValue.getFiniteValue(),
- legend='Max',
- text='Max',
- draggable=maxDraggable,
- color='blue',
- constraint=self._plotMaxMarkerConstraint)
+ posMin = self._minValue.getFiniteValue()
+ posMax = self._maxValue.getFiniteValue()
+
+ def isDisplayable(pos):
+ if scale == Axis.LOGARITHMIC:
+ return pos > 0.0
+ return True
+
+ if isDisplayable(posMin):
+ minDraggable = (self._colormap().isEditable() and
+ not self._minValue.isAutoChecked())
+ self._plot.addXMarker(
+ posMin,
+ legend='Min',
+ text='Min',
+ draggable=minDraggable,
+ color='blue',
+ constraint=self._plotMinMarkerConstraint)
+ if isDisplayable(posMax):
+ maxDraggable = (self._colormap().isEditable() and
+ not self._maxValue.isAutoChecked())
+ self._plot.addXMarker(
+ posMax,
+ legend='Max',
+ text='Max',
+ draggable=maxDraggable,
+ color='blue',
+ constraint=self._plotMaxMarkerConstraint)
self._plot.resetZoom()
@@ -546,7 +650,7 @@ class ColormapDialog(qt.QDialog):
"""Compute the data range as used by :meth:`setDataRange`.
:param data: The data to process
- :rtype: Tuple(float, float, float)
+ :rtype: List[Union[None,float]]
"""
if data is None or len(data) == 0:
return None, None, None
@@ -557,10 +661,7 @@ class ColormapDialog(qt.QDialog):
dataRange = None
if dataRange is not None:
- min_positive = dataRange.min_positive
- if min_positive is None:
- min_positive = float('nan')
- dataRange = dataRange.minimum, min_positive, dataRange.maximum
+ dataRange = dataRange.minimum, dataRange.min_positive, dataRange.maximum
if dataRange is None or len(dataRange) != 3:
qt.QMessageBox.warning(
@@ -571,7 +672,7 @@ class ColormapDialog(qt.QDialog):
return dataRange
@staticmethod
- def computeHistogram(data):
+ def computeHistogram(data, scale=Axis.LINEAR):
"""Compute the data histogram as used by :meth:`setHistogram`.
:param data: The data to process
@@ -588,7 +689,12 @@ class ColormapDialog(qt.QDialog):
if len(_data) == 0:
return None, None
+ if scale == Axis.LOGARITHMIC:
+ _data = numpy.log10(_data)
xmin, xmax = min_max(_data, min_positive=False, finite=True)
+ if xmin is None:
+ return None, None
+
nbins = min(256, int(numpy.sqrt(_data.size)))
data_range = xmin, xmax
@@ -601,7 +707,10 @@ class ColormapDialog(qt.QDialog):
_data = _data.ravel().astype(numpy.float32)
histogram = Histogramnd(_data, n_bins=nbins, histo_range=data_range)
- return histogram.histo, histogram.edges[0]
+ bins = histogram.edges[0]
+ if scale == Axis.LOGARITHMIC:
+ bins = 10**bins
+ return histogram.histo, bins
def _getData(self):
if self._data is None:
@@ -624,7 +733,10 @@ class ColormapDialog(qt.QDialog):
else:
self._data = weakref.ref(data, self._dataAboutToFinalize)
- self._updateDataInPlot()
+ if self.isVisible():
+ self._updateDataInPlot()
+ else:
+ self._displayLater()
def _setDataInPlotMode(self, mode):
if self._dataInPlotMode == mode:
@@ -660,10 +772,15 @@ class ColormapDialog(qt.QDialog):
self.setDataRange(*result)
elif mode == _DataInPlotMode.HISTOGRAM:
# The histogram should be done in a worker thread
- result = self.computeHistogram(data)
+ result = self.computeHistogram(data, scale=self._plot.getXAxis().getScale())
self.setHistogram(*result)
self.setDataRange()
+ def _invalidateHistogram(self):
+ """Recompute the histogram if it is displayed"""
+ if self._dataInPlotMode == _DataInPlotMode.HISTOGRAM:
+ self._updateDataInPlot()
+
def _colormapAboutToFinalize(self, weakrefColormap):
"""Callback when the data weakref is about to be finalized."""
if self._colormap is weakrefColormap:
@@ -727,9 +844,9 @@ class ColormapDialog(qt.QDialog):
"""
colormap = self.getColormap()
if colormap is not None and self._colormapStoredState is not None:
- if self._colormap()._toDict() != self._colormapStoredState:
+ if colormap != self._colormapStoredState:
self._ignoreColormapChange = True
- colormap._setFromDict(self._colormapStoredState)
+ colormap.setFromColormap(self._colormapStoredState)
self._ignoreColormapChange = False
self._applyColormap()
@@ -740,12 +857,18 @@ class ColormapDialog(qt.QDialog):
:param float positiveMin: The positive minimum of the data
:param float maximum: The maximum of the data
"""
- if minimum is None or positiveMin is None or maximum is None:
+ scale = self._plot.getXAxis().getScale()
+ if scale == Axis.LOGARITHMIC:
+ dataMin, dataMax = positiveMin, maximum
+ else:
+ dataMin, dataMax = minimum, maximum
+
+ if dataMin is None or dataMax is None:
self._dataRange = None
self._plot.remove(legend='Range', kind='histogram')
else:
hist = numpy.array([1])
- bin_edges = numpy.array([minimum, maximum])
+ bin_edges = numpy.array([dataMin, dataMax])
self._plot.addHistogram(hist,
bin_edges,
legend="Range",
@@ -801,7 +924,7 @@ class ColormapDialog(qt.QDialog):
"""
colormap = self.getColormap()
if colormap is not None:
- self._colormapStoredState = colormap._toDict()
+ self._colormapStoredState = colormap.copy()
else:
self._colormapStoredState = None
@@ -830,8 +953,11 @@ class ColormapDialog(qt.QDialog):
self._colormap = colormap
self.storeCurrentState()
- self._updateResetButton()
- self._applyColormap()
+ if self.isVisible():
+ self._applyColormap()
+ else:
+ self._updateResetButton()
+ self._displayLater()
def _updateResetButton(self):
resetButton = self._buttonsNonModal.button(qt.QDialogButtonBox.Reset)
@@ -839,7 +965,7 @@ class ColormapDialog(qt.QDialog):
colormap = self.getColormap()
if colormap is not None and colormap.isEditable():
# can reset only in the case the colormap changed
- rStateEnabled = colormap._toDict() != self._colormapStoredState
+ rStateEnabled = colormap != self._colormapStoredState
resetButton.setEnabled(rStateEnabled)
def _applyColormap(self):
@@ -856,12 +982,8 @@ class ColormapDialog(qt.QDialog):
self._maxValue.setEnabled(False)
else:
self._ignoreColormapChange = True
-
- if colormap.getName() is not None:
- name = colormap.getName()
- self._comboBoxColormap.setCurrentName(name)
- self._comboBoxColormap.setEnabled(self._colormap().isEditable())
-
+ self._comboBoxColormap.setCurrentLut(colormap)
+ self._comboBoxColormap.setEnabled(colormap.isEditable())
assert colormap.getNormalization() in Colormap.NORMALIZATIONS
self._normButtonLinear.setChecked(
colormap.getNormalization() == Colormap.LINEAR)
@@ -870,12 +992,17 @@ class ColormapDialog(qt.QDialog):
vmin = colormap.getVMin()
vmax = colormap.getVMax()
dataRange = colormap.getColormapRange()
- self._normButtonLinear.setEnabled(self._colormap().isEditable())
- self._normButtonLog.setEnabled(self._colormap().isEditable())
+ self._normButtonLinear.setEnabled(colormap.isEditable())
+ self._normButtonLog.setEnabled(colormap.isEditable())
self._minValue.setValue(vmin or dataRange[0], isAuto=vmin is None)
self._maxValue.setValue(vmax or dataRange[1], isAuto=vmax is None)
- self._minValue.setEnabled(self._colormap().isEditable())
- self._maxValue.setEnabled(self._colormap().isEditable())
+ self._minValue.setEnabled(colormap.isEditable())
+ self._maxValue.setEnabled(colormap.isEditable())
+
+ axis = self._plot.getXAxis()
+ scale = axis.LINEAR if colormap.getNormalization() == Colormap.LINEAR else axis.LOGARITHMIC
+ axis.setScale(scale)
+
self._ignoreColormapChange = False
self._plotUpdate()
@@ -908,26 +1035,47 @@ class ColormapDialog(qt.QDialog):
self._plotUpdate()
self._updateResetButton()
- def _updateName(self):
+ def _updateLut(self):
if self._ignoreColormapChange is True:
return
- if self._colormap():
+ colormap = self._colormap()
+ if colormap is not None:
self._ignoreColormapChange = True
- self._colormap().setName(
- self._comboBoxColormap.getCurrentName())
+ name = self._comboBoxColormap.getCurrentName()
+ if name is not None:
+ colormap.setName(name)
+ else:
+ lut = self._comboBoxColormap.getCurrentColors()
+ colormap.setColormapLUT(lut)
self._ignoreColormapChange = False
- def _updateLinearNorm(self, isNormLinear):
+ def _updateNormalization(self, button):
if self._ignoreColormapChange is True:
return
+ if not button.isChecked():
+ return
+
+ if button is self._normButtonLinear:
+ norm = Colormap.LINEAR
+ scale = Axis.LINEAR
+ elif button is self._normButtonLog:
+ norm = Colormap.LOGARITHM
+ scale = Axis.LOGARITHMIC
+ else:
+ assert(False)
- if self._colormap():
+ colormap = self.getColormap()
+ if colormap is not None:
self._ignoreColormapChange = True
- norm = Colormap.LINEAR if isNormLinear else Colormap.LOGARITHM
- self._colormap().setNormalization(norm)
+ colormap.setNormalization(norm)
+ axis = self._plot.getXAxis()
+ axis.setScale(scale)
self._ignoreColormapChange = False
+ self._invalidateHistogram()
+ self._updateMinMaxData()
+
def _minMaxTextEdited(self, text):
"""Handle _minValue and _maxValue textEdited signal"""
self._minMaxWasEdited = True
@@ -975,13 +1123,3 @@ class ColormapDialog(qt.QDialog):
else:
# Use QDialog keyPressEvent
super(ColormapDialog, self).keyPressEvent(event)
-
- def _activeLogNorm(self, isLog):
- if self._ignoreColormapChange is True:
- return
- if self._colormap():
- self._ignoreColormapChange = True
- norm = Colormap.LOGARITHM if isLog is True else Colormap.LINEAR
- self._colormap().setNormalization(norm)
- self._ignoreColormapChange = False
- self._updateMinMaxData()
diff --git a/silx/gui/dialog/DataFileDialog.py b/silx/gui/dialog/DataFileDialog.py
index 7ff1258..d2d76a3 100644
--- a/silx/gui/dialog/DataFileDialog.py
+++ b/silx/gui/dialog/DataFileDialog.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -30,16 +30,14 @@ __authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "14/02/2018"
+import enum
import logging
from silx.gui import qt
from silx.gui.hdf5.Hdf5Formatter import Hdf5Formatter
import silx.io
from .AbstractDataFileDialog import AbstractDataFileDialog
-from silx.third_party import enum
-try:
- import fabio
-except ImportError:
- fabio = None
+
+import fabio
_logger = logging.getLogger(__name__)
diff --git a/silx/gui/dialog/FileTypeComboBox.py b/silx/gui/dialog/FileTypeComboBox.py
index 07b11cf..92529bc 100644
--- a/silx/gui/dialog/FileTypeComboBox.py
+++ b/silx/gui/dialog/FileTypeComboBox.py
@@ -28,12 +28,9 @@ This module contains utilitaries used by other dialog modules.
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "06/02/2018"
+__date__ = "17/01/2019"
-try:
- import fabio
-except ImportError:
- fabio = None
+import fabio
import silx.io
from silx.gui import qt
@@ -82,7 +79,7 @@ class FileTypeComboBox(qt.QComboBox):
def __initItems(self):
self.clear()
- if fabio is not None and self.__fabioUrlSupported:
+ if self.__fabioUrlSupported:
self.__insertFabioFormats()
self.__insertSilxFormats()
self.__insertAllSupported()
@@ -138,21 +135,36 @@ class FileTypeComboBox(qt.QComboBox):
def __insertFabioFormats(self):
formats = fabio.fabioformats.get_classes(reader=True)
+ from fabio import fabioutils
+ if hasattr(fabioutils, "COMPRESSED_EXTENSIONS"):
+ compressedExtensions = fabioutils.COMPRESSED_EXTENSIONS
+ else:
+ # Support for fabio < 0.9
+ compressedExtensions = set(["gz", "bz2"])
+
extensions = []
allExtensions = set([])
+ def extensionsIterator(reader):
+ for extension in reader.DEFAULT_EXTENSIONS:
+ yield "*.%s" % extension
+ for compressedExtension in compressedExtensions:
+ for extension in reader.DEFAULT_EXTENSIONS:
+ yield "*.%s.%s" % (extension, compressedExtension)
+
for reader in formats:
if not hasattr(reader, "DESCRIPTION"):
continue
if not hasattr(reader, "DEFAULT_EXTENSIONS"):
continue
- ext = reader.DEFAULT_EXTENSIONS
- ext = ["*.%s" % e for e in ext]
+ displayext = reader.DEFAULT_EXTENSIONS
+ displayext = ["*.%s" % e for e in displayext]
+ ext = list(extensionsIterator(reader))
allExtensions.update(ext)
if ext == []:
ext = ["*"]
- extensions.append((reader.DESCRIPTION, ext, reader.codec_name()))
+ extensions.append((reader.DESCRIPTION, displayext, ext, reader.codec_name()))
extensions = list(sorted(extensions))
allExtensions = list(sorted(list(allExtensions)))
@@ -162,13 +174,14 @@ class FileTypeComboBox(qt.QComboBox):
self.setItemData(index, Codec(any_fabio=True), role=self.CODEC_ROLE)
for e in extensions:
+ description, displayExt, allExt, _codecName = e
index = self.count()
if len(e[1]) < 10:
- self.addItem("%s%s (%s)" % (self.INDENTATION, e[0], " ".join(e[1])))
+ self.addItem("%s%s (%s)" % (self.INDENTATION, description, " ".join(displayExt)))
else:
- self.addItem(e[0])
- codec = Codec(fabio_codec=e[2])
- self.setItemData(index, e[1], role=self.EXTENSIONS_ROLE)
+ self.addItem("%s%s" % (self.INDENTATION, description))
+ codec = Codec(fabio_codec=_codecName)
+ self.setItemData(index, allExt, role=self.EXTENSIONS_ROLE)
self.setItemData(index, codec, role=self.CODEC_ROLE)
def itemExtensions(self, index):
diff --git a/silx/gui/dialog/ImageFileDialog.py b/silx/gui/dialog/ImageFileDialog.py
index c324071..d015bd2 100644
--- a/silx/gui/dialog/ImageFileDialog.py
+++ b/silx/gui/dialog/ImageFileDialog.py
@@ -28,7 +28,7 @@ This module contains an :class:`ImageFileDialog`.
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "12/02/2018"
+__date__ = "05/03/2019"
import logging
from silx.gui.plot import actions
@@ -36,10 +36,6 @@ from silx.gui import qt
from silx.gui.plot.PlotWidget import PlotWidget
from .AbstractDataFileDialog import AbstractDataFileDialog
import silx.io
-try:
- import fabio
-except ImportError:
- fabio = None
_logger = logging.getLogger(__name__)
@@ -64,7 +60,7 @@ class _ImageSelection(qt.QWidget):
def isUsed(self):
if self.__shape is None:
- return None
+ return False
return len(self.__shape) > 2
def getSelectedData(self, data):
@@ -73,6 +69,10 @@ class _ImageSelection(qt.QWidget):
return image
def setData(self, data):
+ if data is None:
+ self.__visibleSliders = 0
+ return
+
shape = data.shape
if self.__shape is not None:
# clean up
@@ -117,6 +117,22 @@ class _ImageSelection(qt.QWidget):
break
self.__axis[i].setValue(value)
+ def selectSlicing(self, slicing):
+ """Select a slicing.
+
+ The provided value could be unconsistent and therefore is not supposed
+ to be retrivable with a getter.
+
+ :param Union[None,Tuple[int]] slicing:
+ """
+ if slicing is None:
+ # Create a default slicing
+ needed = self.__visibleSliders
+ slicing = (0,) * needed
+ if len(slicing) < self.__visibleSliders:
+ slicing = slicing + (0,) * (self.__visibleSliders - len(slicing))
+ self.setSlicing(slicing)
+
class _ImagePreview(qt.QWidget):
"""Provide a preview of the selected image"""
diff --git a/silx/gui/dialog/SafeFileSystemModel.py b/silx/gui/dialog/SafeFileSystemModel.py
index 198e089..26954e3 100644
--- a/silx/gui/dialog/SafeFileSystemModel.py
+++ b/silx/gui/dialog/SafeFileSystemModel.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -34,8 +34,10 @@ import sys
import os.path
import logging
import weakref
+
+import six
+
from silx.gui import qt
-from silx.third_party import six
from .SafeFileIconProvider import SafeFileIconProvider
_logger = logging.getLogger(__name__)
diff --git a/silx/gui/dialog/test/test_colormapdialog.py b/silx/gui/dialog/test/test_colormapdialog.py
index 6e50193..8dad196 100644
--- a/silx/gui/dialog/test/test_colormapdialog.py
+++ b/silx/gui/dialog/test/test_colormapdialog.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -26,13 +26,11 @@
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "23/05/2018"
+__date__ = "09/11/2018"
-import doctest
import unittest
-from silx.gui.utils.testutils import qWaitForWindowExposedAndActivate
from silx.gui import qt
from silx.gui.dialog import ColormapDialog
from silx.gui.utils.testutils import TestCaseQt
@@ -43,27 +41,6 @@ from silx.gui.plot.PlotWindow import PlotWindow
import numpy.random
-# 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."""
-
-
class TestColormapDialog(TestCaseQt, ParametricTestCase):
"""Test the ColormapDialog."""
def setUp(self):
@@ -86,10 +63,12 @@ class TestColormapDialog(TestCaseQt, ParametricTestCase):
editing the same colormap"""
colormapDiag2 = ColormapDialog.ColormapDialog()
colormapDiag2.setColormap(self.colormap)
+ colormapDiag2.show()
self.colormapDiag.setColormap(self.colormap)
+ self.colormapDiag.show()
- self.colormapDiag._comboBoxColormap.setCurrentName('red')
- self.colormapDiag._normButtonLog.setChecked(True)
+ self.colormapDiag._comboBoxColormap._setCurrentName('red')
+ self.colormapDiag._normButtonLog.click()
self.assertTrue(self.colormap.getName() == 'red')
self.assertTrue(self.colormapDiag.getColormap().getName() == 'red')
self.assertTrue(self.colormap.getNormalization() == 'log')
@@ -178,6 +157,7 @@ class TestColormapDialog(TestCaseQt, ParametricTestCase):
def testSetColormapIsCorrect(self):
"""Make sure the interface fir the colormap when set a new colormap"""
self.colormap.setName('red')
+ self.colormapDiag.show()
for norm in (Colormap.NORMALIZATIONS):
for autoscale in (True, False):
if autoscale is True:
@@ -211,7 +191,7 @@ class TestColormapDialog(TestCaseQt, ParametricTestCase):
self.colormapDiag.show()
del self.colormap
self.assertTrue(self.colormapDiag.getColormap() is None)
- self.colormapDiag._comboBoxColormap.setCurrentName('blue')
+ self.colormapDiag._comboBoxColormap._setCurrentName('blue')
def testColormapEditedOutside(self):
"""Make sure the GUI is still up to date if the colormap is modified
@@ -274,7 +254,7 @@ class TestColormapDialog(TestCaseQt, ParametricTestCase):
cb = self.colormapDiag._comboBoxColormap
self.assertTrue(cb.getCurrentName() == colormapName)
cb.setCurrentIndex(0)
- index = cb.findColormap(colormapName)
+ index = cb.findLutName(colormapName)
assert index is not 0 # if 0 then the rest of the test has no sense
cb.setCurrentIndex(index)
self.assertTrue(cb.getCurrentName() == colormapName)
@@ -283,6 +263,7 @@ class TestColormapDialog(TestCaseQt, ParametricTestCase):
"""Test that the colormapDialog is correctly updated when changing the
colormap editable status"""
colormap = Colormap(normalization='linear', vmin=1.0, vmax=10.0)
+ self.colormapDiag.show()
self.colormapDiag.setColormap(colormap)
for editable in (True, False):
with self.subTest(editable=editable):
@@ -302,7 +283,7 @@ class TestColormapDialog(TestCaseQt, ParametricTestCase):
# False
self.colormapDiag.setModal(False)
colormap.setEditable(True)
- self.colormapDiag._normButtonLog.setChecked(True)
+ self.colormapDiag._normButtonLog.click()
resetButton = self.colormapDiag._buttonsNonModal.button(qt.QDialogButtonBox.Reset)
self.assertTrue(resetButton.isEnabled())
colormap.setEditable(False)
@@ -387,7 +368,6 @@ class TestColormapAction(TestCaseQt):
def suite():
test_suite = unittest.TestSuite()
- test_suite.addTest(cmapDocTestSuite)
for testClass in (TestColormapDialog, TestColormapAction):
test_suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(
testClass))
diff --git a/silx/gui/dialog/test/test_datafiledialog.py b/silx/gui/dialog/test/test_datafiledialog.py
index aff6bc4..b60ea12 100644
--- a/silx/gui/dialog/test/test_datafiledialog.py
+++ b/silx/gui/dialog/test/test_datafiledialog.py
@@ -26,7 +26,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "05/10/2018"
+__date__ = "08/03/2019"
import unittest
@@ -36,16 +36,8 @@ import shutil
import os
import io
import weakref
-
-try:
- import fabio
-except ImportError:
- fabio = None
-try:
- import h5py
-except ImportError:
- h5py = None
-
+import fabio
+import h5py
import silx.io.url
from silx.gui import qt
from silx.gui.utils import testutils
@@ -62,36 +54,33 @@ def setUpModule():
data = numpy.arange(100 * 100)
data.shape = 100, 100
- if fabio is not None:
- filename = _tmpDirectory + "/singleimage.edf"
- image = fabio.edfimage.EdfImage(data=data)
- image.write(filename)
-
- if h5py is not None:
- filename = _tmpDirectory + "/data.h5"
- f = h5py.File(filename, "w")
- f["scalar"] = 10
- f["image"] = data
- f["cube"] = [data, data + 1, data + 2]
- f["complex_image"] = data * 1j
- f["group/image"] = data
- f["nxdata/foo"] = 10
- f["nxdata"].attrs["NX_class"] = u"NXdata"
- f.close()
-
- if h5py is not None:
- directory = os.path.join(_tmpDirectory, "data")
- os.mkdir(directory)
- filename = os.path.join(directory, "data.h5")
- f = h5py.File(filename, "w")
- f["scalar"] = 10
- f["image"] = data
- f["cube"] = [data, data + 1, data + 2]
- f["complex_image"] = data * 1j
- f["group/image"] = data
- f["nxdata/foo"] = 10
- f["nxdata"].attrs["NX_class"] = u"NXdata"
- f.close()
+ filename = _tmpDirectory + "/singleimage.edf"
+ image = fabio.edfimage.EdfImage(data=data)
+ image.write(filename)
+
+ filename = _tmpDirectory + "/data.h5"
+ f = h5py.File(filename, "w")
+ f["scalar"] = 10
+ f["image"] = data
+ f["cube"] = [data, data + 1, data + 2]
+ f["complex_image"] = data * 1j
+ f["group/image"] = data
+ f["nxdata/foo"] = 10
+ f["nxdata"].attrs["NX_class"] = u"NXdata"
+ f.close()
+
+ directory = os.path.join(_tmpDirectory, "data")
+ os.mkdir(directory)
+ filename = os.path.join(directory, "data.h5")
+ f = h5py.File(filename, "w")
+ f["scalar"] = 10
+ f["image"] = data
+ f["cube"] = [data, data + 1, data + 2]
+ f["complex_image"] = data * 1j
+ f["group/image"] = data
+ f["nxdata/foo"] = 10
+ f["nxdata"].attrs["NX_class"] = u"NXdata"
+ f.close()
filename = _tmpDirectory + "/badformat.h5"
with io.open(filename, "wb") as f:
@@ -141,7 +130,7 @@ class _UtilsMixin(object):
path2_ = os.path.normcase(path2)
if path1_ == path2_:
# Use the unittest API to log and display error
- self.assertNotEquals(path1, path2)
+ self.assertNotEqual(path1, path2)
class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
@@ -185,8 +174,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertEqual(dialog.result(), qt.QDialog.Rejected)
def testSelectRoot_Activate(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
dialog.show()
@@ -211,8 +198,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertEqual(dialog.result(), qt.QDialog.Accepted)
def testSelectGroup_Activate(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
dialog.show()
@@ -243,8 +228,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertEqual(dialog.result(), qt.QDialog.Accepted)
def testSelectDataset_Activate(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
dialog.show()
@@ -275,8 +258,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertEqual(dialog.result(), qt.QDialog.Accepted)
def testClickOnBackToParentTool(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -307,8 +288,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertSamePath(url.text(), _tmpDirectory)
def testClickOnBackToRootTool(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -332,8 +311,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
# self.assertFalse(button.isEnabled())
def testClickOnBackToDirectoryTool(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -361,8 +338,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.allowedLeakingWidgets = 1
def testClickOnHistoryTools(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -402,8 +377,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertSamePath(url.text(), path3)
def testSelectImageFromEdf(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -412,13 +385,11 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
filename = _tmpDirectory + "/singleimage.edf"
url = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/scan_0/instrument/detector_0/data")
dialog.selectUrl(url.path())
- self.assertTrue(dialog._selectedData().shape, (100, 100))
+ self.assertEqual(dialog._selectedData().shape, (100, 100))
self.assertSamePath(dialog.selectedFile(), filename)
self.assertSamePath(dialog.selectedUrl(), url.path())
def testSelectImage(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -428,13 +399,11 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/image").path()
dialog.selectUrl(path)
# test
- self.assertTrue(dialog._selectedData().shape, (100, 100))
+ self.assertEqual(dialog._selectedData().shape, (100, 100))
self.assertSamePath(dialog.selectedFile(), filename)
self.assertSamePath(dialog.selectedUrl(), path)
def testSelectScalar(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -449,8 +418,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertSamePath(dialog.selectedUrl(), path)
def testSelectGroup(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -467,8 +434,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertSamePath(uri.data_path(), "/group")
def testSelectRoot(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -485,8 +450,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertSamePath(uri.data_path(), "/")
def testSelectH5_Activate(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -516,11 +479,12 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.qWaitForPendingActions(dialog)
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
filename = _tmpDirectory + "/badformat.h5"
- index = browser.rootIndex().model().index(filename)
+ index = browser.model().index(filename)
+ browser.selectIndex(index)
browser.activated.emit(index)
self.qWaitForPendingActions(dialog)
# test
- self.assertTrue(dialog.selectedUrl(), filename)
+ self.assertSamePath(dialog.selectedUrl(), filename)
def _countSelectableItems(self, model, rootIndex):
selectable = 0
@@ -533,10 +497,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
return selectable
def testFilterExtensions(self):
- if h5py is None:
- self.skipTest("h5py is missing")
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
dialog.show()
@@ -558,8 +518,6 @@ class TestDataFileDialog_FilterDataset(testutils.TestCaseQt, _UtilsMixin):
return dialog
def testSelectGroup_Activate(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
dialog.show()
@@ -585,8 +543,6 @@ class TestDataFileDialog_FilterDataset(testutils.TestCaseQt, _UtilsMixin):
self.assertFalse(button.isEnabled())
def testSelectDataset_Activate(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
dialog.show()
@@ -632,8 +588,6 @@ class TestDataFileDialog_FilterGroup(testutils.TestCaseQt, _UtilsMixin):
return dialog
def testSelectGroup_Activate(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
dialog.show()
@@ -666,8 +620,6 @@ class TestDataFileDialog_FilterGroup(testutils.TestCaseQt, _UtilsMixin):
self.assertRaises(Exception, dialog.selectedData)
def testSelectDataset_Activate(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
dialog.show()
@@ -711,8 +663,6 @@ class TestDataFileDialog_FilterNXdata(testutils.TestCaseQt, _UtilsMixin):
return dialog
def testSelectGroupRefused_Activate(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
dialog.show()
@@ -740,8 +690,6 @@ class TestDataFileDialog_FilterNXdata(testutils.TestCaseQt, _UtilsMixin):
self.assertRaises(Exception, dialog.selectedData)
def testSelectNXdataAccepted_Activate(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
dialog.show()
@@ -906,7 +854,7 @@ class TestDataFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
dialog2 = self.createDialog()
result = dialog2.restoreState(state)
self.assertTrue(result)
- self.assertNotEquals(dialog2.directory(), directory)
+ self.assertNotEqual(dialog2.directory(), directory)
def testHistory(self):
dialog = self.createDialog()
@@ -944,8 +892,6 @@ class TestDataFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
self.assertIsNone(dialog._selectedData())
def testBadSubpath(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
self.qWaitForPendingActions(dialog)
@@ -965,8 +911,6 @@ class TestDataFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
self.assertEqual(url.data_path(), "/group")
def testUnsupportedSlicingPath(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
self.qWaitForPendingActions(dialog)
dialog.selectUrl(_tmpDirectory + "/data.h5?path=/cube&slice=0")
diff --git a/silx/gui/dialog/test/test_imagefiledialog.py b/silx/gui/dialog/test/test_imagefiledialog.py
index 66469f3..c019afb 100644
--- a/silx/gui/dialog/test/test_imagefiledialog.py
+++ b/silx/gui/dialog/test/test_imagefiledialog.py
@@ -26,7 +26,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "05/10/2018"
+__date__ = "08/03/2019"
import unittest
@@ -36,16 +36,8 @@ import shutil
import os
import io
import weakref
-
-try:
- import fabio
-except ImportError:
- fabio = None
-try:
- import h5py
-except ImportError:
- h5py = None
-
+import fabio
+import h5py
import silx.io.url
from silx.gui import qt
from silx.gui.utils import testutils
@@ -63,42 +55,39 @@ def setUpModule():
data = numpy.arange(100 * 100)
data.shape = 100, 100
- if fabio is not None:
- filename = _tmpDirectory + "/singleimage.edf"
- image = fabio.edfimage.EdfImage(data=data)
- image.write(filename)
+ filename = _tmpDirectory + "/singleimage.edf"
+ image = fabio.edfimage.EdfImage(data=data)
+ image.write(filename)
- filename = _tmpDirectory + "/multiframe.edf"
- image = fabio.edfimage.EdfImage(data=data)
- image.appendFrame(data=data + 1)
- image.appendFrame(data=data + 2)
- image.write(filename)
+ filename = _tmpDirectory + "/multiframe.edf"
+ image = fabio.edfimage.EdfImage(data=data)
+ image.appendFrame(data=data + 1)
+ image.appendFrame(data=data + 2)
+ image.write(filename)
- filename = _tmpDirectory + "/singleimage.msk"
- image = fabio.fit2dmaskimage.Fit2dMaskImage(data=data % 2 == 1)
- image.write(filename)
+ filename = _tmpDirectory + "/singleimage.msk"
+ image = fabio.fit2dmaskimage.Fit2dMaskImage(data=data % 2 == 1)
+ image.write(filename)
- if h5py is not None:
- filename = _tmpDirectory + "/data.h5"
- f = h5py.File(filename, "w")
+ filename = _tmpDirectory + "/data.h5"
+ with h5py.File(filename, "w") as f:
f["scalar"] = 10
f["image"] = data
f["cube"] = [data, data + 1, data + 2]
+ f["single_frame"] = [data + 5]
f["complex_image"] = data * 1j
f["group/image"] = data
- f.close()
- if h5py is not None:
- directory = os.path.join(_tmpDirectory, "data")
- os.mkdir(directory)
- filename = os.path.join(directory, "data.h5")
- f = h5py.File(filename, "w")
+ directory = os.path.join(_tmpDirectory, "data")
+ os.mkdir(directory)
+ filename = os.path.join(directory, "data.h5")
+ with h5py.File(filename, "w") as f:
f["scalar"] = 10
f["image"] = data
f["cube"] = [data, data + 1, data + 2]
+ f["single_frame"] = [data + 5]
f["complex_image"] = data * 1j
f["group/image"] = data
- f.close()
filename = _tmpDirectory + "/badformat.edf"
with io.open(filename, "wb") as f:
@@ -148,7 +137,7 @@ class _UtilsMixin(object):
path2_ = os.path.normcase(path2)
if path1_ == path2_:
# Use the unittest API to log and display error
- self.assertNotEquals(path1, path2)
+ self.assertNotEqual(path1, path2)
class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
@@ -192,8 +181,6 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertEqual(dialog.result(), qt.QDialog.Rejected)
def testDisplayAndClickOpen(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -259,8 +246,6 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertEqual(dialog.viewMode(), qt.QFileDialog.List)
def testClickOnBackToParentTool(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -291,8 +276,6 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertSamePath(url.text(), _tmpDirectory)
def testClickOnBackToRootTool(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -316,8 +299,6 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
# self.assertFalse(button.isEnabled())
def testClickOnBackToDirectoryTool(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -345,8 +326,6 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.allowedLeakingWidgets = 1
def testClickOnHistoryTools(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -386,8 +365,6 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertSamePath(url.text(), path3)
def testSelectImageFromEdf(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -396,14 +373,12 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
filename = _tmpDirectory + "/singleimage.edf"
path = filename
dialog.selectUrl(path)
- self.assertTrue(dialog.selectedImage().shape, (100, 100))
+ self.assertEqual(dialog.selectedImage().shape, (100, 100))
self.assertSamePath(dialog.selectedFile(), filename)
path = silx.io.url.DataUrl(scheme="fabio", file_path=filename).path()
self.assertSamePath(dialog.selectedUrl(), path)
def testSelectImageFromEdf_Activate(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -421,13 +396,11 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
browser.activated.emit(index)
self.qWaitForPendingActions(dialog)
# test
- self.assertTrue(dialog.selectedImage().shape, (100, 100))
+ self.assertEqual(dialog.selectedImage().shape, (100, 100))
self.assertSamePath(dialog.selectedFile(), filename)
self.assertSamePath(dialog.selectedUrl(), path)
def testSelectFrameFromEdf(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -438,14 +411,12 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
dialog.selectUrl(path)
# test
image = dialog.selectedImage()
- self.assertTrue(image.shape, (100, 100))
- self.assertTrue(image[0, 0], 1)
+ self.assertEqual(image.shape, (100, 100))
+ self.assertEqual(image[0, 0], 1)
self.assertSamePath(dialog.selectedFile(), filename)
self.assertSamePath(dialog.selectedUrl(), path)
def testSelectImageFromMsk(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -455,13 +426,11 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
path = silx.io.url.DataUrl(scheme="fabio", file_path=filename).path()
dialog.selectUrl(path)
# test
- self.assertTrue(dialog.selectedImage().shape, (100, 100))
+ self.assertEqual(dialog.selectedImage().shape, (100, 100))
self.assertSamePath(dialog.selectedFile(), filename)
self.assertSamePath(dialog.selectedUrl(), path)
def testSelectImageFromH5(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -471,13 +440,11 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/image").path()
dialog.selectUrl(path)
# test
- self.assertTrue(dialog.selectedImage().shape, (100, 100))
+ self.assertEqual(dialog.selectedImage().shape, (100, 100))
self.assertSamePath(dialog.selectedFile(), filename)
self.assertSamePath(dialog.selectedUrl(), path)
def testSelectH5_Activate(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -498,8 +465,6 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertSamePath(dialog.selectedUrl(), path)
def testSelectFrameFromH5(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -509,8 +474,23 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/cube", data_slice=(1, )).path()
dialog.selectUrl(path)
# test
- self.assertTrue(dialog.selectedImage().shape, (100, 100))
- self.assertTrue(dialog.selectedImage()[0, 0], 1)
+ self.assertEqual(dialog.selectedImage().shape, (100, 100))
+ self.assertEqual(dialog.selectedImage()[0, 0], 1)
+ self.assertSamePath(dialog.selectedFile(), filename)
+ self.assertSamePath(dialog.selectedUrl(), path)
+
+ def testSelectSingleFrameFromH5(self):
+ dialog = self.createDialog()
+ dialog.show()
+ self.qWaitForWindowExposed(dialog)
+
+ # init state
+ filename = _tmpDirectory + "/data.h5"
+ path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/single_frame", data_slice=(0, )).path()
+ dialog.selectUrl(path)
+ # test
+ self.assertEqual(dialog.selectedImage().shape, (100, 100))
+ self.assertEqual(dialog.selectedImage()[0, 0], 5)
self.assertSamePath(dialog.selectedFile(), filename)
self.assertSamePath(dialog.selectedUrl(), path)
@@ -524,11 +504,12 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.qWaitForPendingActions(dialog)
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
filename = _tmpDirectory + "/badformat.edf"
- index = browser.rootIndex().model().index(filename)
+ index = browser.model().index(filename)
+ browser.selectIndex(index)
browser.activated.emit(index)
self.qWaitForPendingActions(dialog)
# test
- self.assertTrue(dialog.selectedUrl(), filename)
+ self.assertSamePath(dialog.selectedUrl(), filename)
def _countSelectableItems(self, model, rootIndex):
selectable = 0
@@ -541,10 +522,6 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
return selectable
def testFilterExtensions(self):
- if h5py is None:
- self.skipTest("h5py is missing")
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
filters = testutils.findChildren(dialog, qt.QWidget, name="fileTypeCombo")[0]
@@ -588,7 +565,7 @@ class TestImageFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
result = dialog2.restoreState(state)
self.qWaitForPendingActions(dialog2)
self.assertTrue(result)
- self.assertTrue(dialog2.colormap().getNormalization(), "log")
+ self.assertEqual(dialog2.colormap().getNormalization(), "log")
def printState(self):
"""
@@ -685,7 +662,7 @@ class TestImageFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
result = dialog.restoreState(state)
self.assertTrue(result)
colormap = dialog.colormap()
- self.assertTrue(colormap.getNormalization(), "log")
+ self.assertEqual(colormap.getNormalization(), "log")
def testRestoreRobusness(self):
"""What's happen if you try to open a config file with a different
@@ -711,7 +688,7 @@ class TestImageFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
dialog2 = self.createDialog()
result = dialog2.restoreState(state)
self.assertTrue(result)
- self.assertNotEquals(dialog2.directory(), directory)
+ self.assertNotEqual(dialog2.directory(), directory)
def testHistory(self):
dialog = self.createDialog()
@@ -745,16 +722,12 @@ class TestImageFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
self.assertSamePath(dialog.directory(), _tmpDirectory)
def testBadDataType(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.selectUrl(_tmpDirectory + "/data.h5::/complex_image")
self.qWaitForPendingActions(dialog)
self.assertIsNone(dialog._selectedData())
def testBadDataShape(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.selectUrl(_tmpDirectory + "/data.h5::/unknown")
self.qWaitForPendingActions(dialog)
@@ -773,8 +746,6 @@ class TestImageFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
self.assertIsNone(dialog._selectedData())
def testBadSubpath(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
self.qWaitForPendingActions(dialog)
@@ -794,8 +765,6 @@ class TestImageFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
self.assertEqual(url.data_path(), "/group")
def testBadSlicingPath(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
self.qWaitForPendingActions(dialog)
dialog.selectUrl(_tmpDirectory + "/data.h5::/cube[a;45,-90]")
diff --git a/silx/gui/dialog/utils.py b/silx/gui/dialog/utils.py
index 1c16b44..e2334f9 100644
--- a/silx/gui/dialog/utils.py
+++ b/silx/gui/dialog/utils.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -33,8 +33,10 @@ __date__ = "25/10/2017"
import os
import sys
import types
+
+import six
+
from silx.gui import qt
-from silx.third_party import six
def samefile(path1, path2):
diff --git a/silx/gui/hdf5/Hdf5Formatter.py b/silx/gui/hdf5/Hdf5Formatter.py
index 6802142..5754fe8 100644
--- a/silx/gui/hdf5/Hdf5Formatter.py
+++ b/silx/gui/hdf5/Hdf5Formatter.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -30,14 +30,12 @@ __license__ = "MIT"
__date__ = "06/06/2018"
import numpy
-from silx.third_party import six
+import six
+
from silx.gui import qt
from silx.gui.data.TextFormatter import TextFormatter
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
class Hdf5Formatter(qt.QObject):
@@ -162,10 +160,9 @@ class Hdf5Formatter(qt.QObject):
compound = [self.humanReadableDType(d) for d in compound]
return "compound(%s)" % ", ".join(compound)
elif numpy.issubdtype(dtype, numpy.integer):
- if h5py is not None:
- enumType = h5py.check_dtype(enum=dtype)
- if enumType is not None:
- return "enum"
+ enumType = h5py.check_dtype(enum=dtype)
+ if enumType is not None:
+ return "enum"
text = str(dtype.newbyteorder('N'))
if numpy.issubdtype(dtype, numpy.floating):
diff --git a/silx/gui/hdf5/Hdf5Item.py b/silx/gui/hdf5/Hdf5Item.py
index b3c313e..6ea870f 100644
--- a/silx/gui/hdf5/Hdf5Item.py
+++ b/silx/gui/hdf5/Hdf5Item.py
@@ -25,11 +25,12 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "03/09/2018"
+__date__ = "17/01/2019"
import logging
import collections
+
from .. import qt
from .. import icons
from . import _utils
@@ -37,7 +38,6 @@ from .Hdf5Node import Hdf5Node
import silx.io.utils
from silx.gui.data.TextFormatter import TextFormatter
from ..hdf5.Hdf5Formatter import Hdf5Formatter
-from ...third_party import six
_logger = logging.getLogger(__name__)
_formatter = TextFormatter()
_hdf5Formatter = Hdf5Formatter(textFormatter=_formatter)
@@ -217,14 +217,32 @@ class Hdf5Item(Hdf5Node):
def _populateChild(self, populateAll=False):
if self.isGroupObj():
- for name in self.obj:
+ keys = []
+ try:
+ for name in self.obj:
+ keys.append(name)
+ except Exception:
+ lib_name = self.obj.__class__.__module__.split(".")[0]
+ _logger.error("Internal %s error. The file is corrupted.", lib_name)
+ _logger.debug("Backtrace", exc_info=True)
+ if keys == []:
+ # If the file was open in READ_ONLY we still can reach something
+ # https://github.com/silx-kit/silx/issues/2262
+ try:
+ for name in self.obj:
+ keys.append(name)
+ except Exception:
+ lib_name = self.obj.__class__.__module__.split(".")[0]
+ _logger.error("Internal %s error (second time). The file is corrupted.", lib_name)
+ _logger.debug("Backtrace", exc_info=True)
+ for name in keys:
try:
class_ = self.obj.get(name, getclass=True)
link = self.obj.get(name, getclass=True, getlink=True)
link = silx.io.utils.get_h5_class(class_=link)
except Exception:
lib_name = self.obj.__class__.__module__.split(".")[0]
- _logger.warning("Internal %s error", lib_name, exc_info=True)
+ _logger.error("Internal %s error", lib_name)
_logger.debug("Backtrace", exc_info=True)
class_ = None
try:
@@ -344,14 +362,12 @@ class Hdf5Item(Hdf5Node):
def nexusClassName(self):
"""Returns the Nexus class name"""
if self.__nx_class is None:
- self.__nx_class = self.obj.attrs.get("NX_class", None)
- if self.__nx_class is None:
- self.__nx_class = ""
+ obj = self.obj.attrs.get("NX_class", None)
+ if obj is None:
+ text = ""
else:
- if six.PY2:
- self.__nx_class = self.__nx_class.decode()
- elif not isinstance(self.__nx_class, str):
- self.__nx_class = str(self.__nx_class, "UTF-8")
+ text = self._getFormatter().textFormatter().toString(obj)
+ self.__nx_class = text.strip('"')
return self.__nx_class
def dataName(self, role):
diff --git a/silx/gui/hdf5/Hdf5TreeModel.py b/silx/gui/hdf5/Hdf5TreeModel.py
index 438200b..152f3e5 100644
--- a/silx/gui/hdf5/Hdf5TreeModel.py
+++ b/silx/gui/hdf5/Hdf5TreeModel.py
@@ -25,7 +25,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "08/10/2018"
+__date__ = "12/03/2019"
import os
@@ -360,9 +360,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
def mimeTypes(self):
types = []
- if self.__fileMoveEnabled:
- types.append(_utils.Hdf5NodeMimeData.MIME_TYPE)
- if self.__datasetDragEnabled:
+ if self.__fileMoveEnabled or self.__datasetDragEnabled:
types.append(_utils.Hdf5DatasetMimeData.MIME_TYPE)
return types
@@ -386,7 +384,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
node = self.nodeFromIndex(indexes[0])
if self.__fileMoveEnabled and node.parent is self.__root:
- mimeData = _utils.Hdf5NodeMimeData(node=node)
+ mimeData = _utils.Hdf5DatasetMimeData(node=node, isRoot=True)
elif self.__datasetDragEnabled:
mimeData = _utils.Hdf5DatasetMimeData(node=node)
else:
@@ -413,23 +411,24 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
if action == qt.Qt.IgnoreAction:
return True
- if self.__fileMoveEnabled and mimedata.hasFormat(_utils.Hdf5NodeMimeData.MIME_TYPE):
- dragNode = mimedata.node()
- parentNode = self.nodeFromIndex(parentIndex)
- if parentNode is not dragNode.parent:
- return False
+ if self.__fileMoveEnabled and mimedata.hasFormat(_utils.Hdf5DatasetMimeData.MIME_TYPE):
+ if mimedata.isRoot():
+ dragNode = mimedata.node()
+ parentNode = self.nodeFromIndex(parentIndex)
+ if parentNode is not dragNode.parent:
+ return False
- if row == -1:
- # append to the parent
- row = parentNode.childCount()
- else:
- # insert at row
- pass
+ if row == -1:
+ # append to the parent
+ row = parentNode.childCount()
+ else:
+ # insert at row
+ pass
- dragNodeParent = dragNode.parent
- sourceRow = dragNodeParent.indexOfChild(dragNode)
- self.moveRow(parentIndex, sourceRow, parentIndex, row)
- return True
+ dragNodeParent = dragNode.parent
+ sourceRow = dragNodeParent.indexOfChild(dragNode)
+ self.moveRow(parentIndex, sourceRow, parentIndex, row)
+ return True
if self.__fileDropEnabled and mimedata.hasFormat("text/uri-list"):
@@ -571,7 +570,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
drag-and-drop"""
obj = node.obj
for f in self.__openedFiles:
- if f in obj:
+ if f is obj:
_logger.debug("Close file %s", obj.filename)
obj.close()
self.__openedFiles.remove(obj)
diff --git a/silx/gui/hdf5/NexusSortFilterProxyModel.py b/silx/gui/hdf5/NexusSortFilterProxyModel.py
index 216e992..9c3533f 100644
--- a/silx/gui/hdf5/NexusSortFilterProxyModel.py
+++ b/silx/gui/hdf5/NexusSortFilterProxyModel.py
@@ -25,7 +25,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "24/07/2018"
+__date__ = "29/11/2018"
import logging
@@ -108,6 +108,8 @@ class NexusSortFilterProxyModel(qt.QSortFilterProxyModel):
def __isNXnode(self, node):
"""Returns true if the node is an NX concept"""
+ if not hasattr(node, "h5Class"):
+ return False
class_ = node.h5Class
if class_ is None or class_ != silx.io.utils.H5Type.GROUP:
return False
diff --git a/silx/gui/hdf5/_utils.py b/silx/gui/hdf5/_utils.py
index 6a34933..aaab228 100644
--- a/silx/gui/hdf5/_utils.py
+++ b/silx/gui/hdf5/_utils.py
@@ -28,12 +28,15 @@ package `silx.gui.hdf5` package.
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "04/05/2018"
+__date__ = "17/01/2019"
import logging
-from .. import qt
+import os.path
+
import silx.io.utils
+import silx.io.url
+from .. import qt
from silx.utils.html import escape
_logger = logging.getLogger(__name__)
@@ -107,11 +110,22 @@ class Hdf5DatasetMimeData(qt.QMimeData):
MIME_TYPE = "application/x-internal-h5py-dataset"
- def __init__(self, node=None, dataset=None):
+ SILX_URI_TYPE = "application/x-silx-uri"
+
+ def __init__(self, node=None, dataset=None, isRoot=False):
qt.QMimeData.__init__(self)
self.__dataset = dataset
self.__node = node
+ self.__isRoot = isRoot
self.setData(self.MIME_TYPE, "".encode(encoding='utf-8'))
+ if node is not None:
+ h5Node = H5Node(node)
+ silxUrl = h5Node.url
+ self.setText(silxUrl)
+ self.setData(self.SILX_URI_TYPE, silxUrl.encode(encoding='utf-8'))
+
+ def isRoot(self):
+ return self.__isRoot
def node(self):
return self.__node
@@ -122,20 +136,6 @@ class Hdf5DatasetMimeData(qt.QMimeData):
return self.__dataset
-class Hdf5NodeMimeData(qt.QMimeData):
- """Mimedata class to identify an internal drag and drop of a Hdf5Node."""
-
- MIME_TYPE = "application/x-internal-h5py-node"
-
- def __init__(self, node=None):
- qt.QMimeData.__init__(self)
- self.__node = node
- self.setData(self.MIME_TYPE, "".encode(encoding='utf-8'))
-
- def node(self):
- return self.__node
-
-
class H5Node(object):
"""Adapter over an h5py object to provide missing informations from h5py
nodes, like internal node path and filename (which are not provided by
@@ -419,3 +419,43 @@ class H5Node(object):
:rtype: str
"""
return self.physical_name.split("/")[-1]
+
+ @property
+ def data_url(self):
+ """Returns a :class:`silx.io.url.DataUrl` object identify this node in the file
+ system.
+
+ :rtype: ~silx.io.url.DataUrl
+ """
+ absolute_filename = os.path.abspath(self.local_filename)
+ return silx.io.url.DataUrl(scheme="silx",
+ file_path=absolute_filename,
+ data_path=self.local_name)
+
+ @property
+ def url(self):
+ """Returns an URL object identifying this node in the file
+ system.
+
+ This URL can be used in different ways.
+
+ .. code-block:: python
+
+ # Parsing the URL
+ import silx.io.url
+ dataurl = silx.io.url.DataUrl(item.url)
+ # dataurl provides access to URL fields
+
+ # Open a numpy array
+ import silx.io
+ dataset = silx.io.get_data(item.url)
+
+ # Open an hdf5 object (URL targetting a file or a group)
+ import silx.io
+ with silx.io.open(item.url) as h5:
+ ...your stuff...
+
+ :rtype: str
+ """
+ data_url = self.data_url
+ return data_url.path()
diff --git a/silx/gui/hdf5/test/test_hdf5.py b/silx/gui/hdf5/test/test_hdf5.py
index 1751a21..0ab4dc4 100644
--- a/silx/gui/hdf5/test/test_hdf5.py
+++ b/silx/gui/hdf5/test/test_hdf5.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -26,7 +26,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "03/05/2018"
+__date__ = "12/03/2019"
import time
@@ -43,10 +43,7 @@ from silx.gui.utils.testutils import SignalListener
from silx.io import commonh5
import weakref
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
_tmpDirectory = None
@@ -56,14 +53,13 @@ def setUpModule():
global _tmpDirectory
_tmpDirectory = tempfile.mkdtemp(prefix=__name__)
- if h5py is not None:
- filename = _tmpDirectory + "/data.h5"
+ filename = _tmpDirectory + "/data.h5"
- # create h5 data
- f = h5py.File(filename, "w")
- g = f.create_group("arrays")
- g.create_dataset("scalar", data=10)
- f.close()
+ # create h5 data
+ f = h5py.File(filename, "w")
+ g = f.create_group("arrays")
+ g.create_dataset("scalar", data=10)
+ f.close()
def tearDownModule():
@@ -91,8 +87,6 @@ class TestHdf5TreeModel(TestCaseQt):
def setUp(self):
super(TestHdf5TreeModel, self).setUp()
- if h5py is None:
- self.skipTest("h5py is not available")
def waitForPendingOperations(self, model):
for _ in range(10):
@@ -127,8 +121,6 @@ class TestHdf5TreeModel(TestCaseQt):
model.appendFile(filename)
self.assertEqual(model.rowCount(qt.QModelIndex()), 1)
# clean up
- index = model.index(0, 0, qt.QModelIndex())
- h5File = model.data(index, hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE)
ref = weakref.ref(model)
model = None
self.qWaitForDestroy(ref)
@@ -232,7 +224,7 @@ class TestHdf5TreeModel(TestCaseQt):
def testSupportedDrop(self):
model = hdf5.Hdf5TreeModel()
- self.assertNotEquals(model.supportedDropActions(), 0)
+ self.assertNotEqual(model.supportedDropActions(), 0)
model.setFileMoveEnabled(False)
model.setFileDropEnabled(False)
@@ -240,11 +232,42 @@ class TestHdf5TreeModel(TestCaseQt):
model.setFileMoveEnabled(False)
model.setFileDropEnabled(True)
- self.assertNotEquals(model.supportedDropActions(), 0)
+ self.assertNotEqual(model.supportedDropActions(), 0)
model.setFileMoveEnabled(True)
model.setFileDropEnabled(False)
- self.assertNotEquals(model.supportedDropActions(), 0)
+ self.assertNotEqual(model.supportedDropActions(), 0)
+
+ def testCloseFile(self):
+ """A file inserted as a filename is open and closed internally."""
+ filename = _tmpDirectory + "/data.h5"
+ model = hdf5.Hdf5TreeModel()
+ self.assertEqual(model.rowCount(qt.QModelIndex()), 0)
+ model.insertFile(filename)
+ self.assertEqual(model.rowCount(qt.QModelIndex()), 1)
+ index = model.index(0, 0)
+ h5File = model.data(index, role=hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE)
+ model.removeIndex(index)
+ self.assertEqual(model.rowCount(qt.QModelIndex()), 0)
+ self.assertFalse(bool(h5File.id.valid), "The HDF5 file was not closed")
+
+ def testNotCloseFile(self):
+ """A file inserted as an h5py object is not open (then not closed)
+ internally."""
+ filename = _tmpDirectory + "/data.h5"
+ try:
+ h5File = h5py.File(filename)
+ model = hdf5.Hdf5TreeModel()
+ self.assertEqual(model.rowCount(qt.QModelIndex()), 0)
+ model.insertH5pyObject(h5File)
+ self.assertEqual(model.rowCount(qt.QModelIndex()), 1)
+ index = model.index(0, 0)
+ h5File = model.data(index, role=hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE)
+ model.removeIndex(index)
+ self.assertEqual(model.rowCount(qt.QModelIndex()), 0)
+ self.assertTrue(bool(h5File.id.valid), "The HDF5 file was unexpetedly closed")
+ finally:
+ h5File.close()
def testDropExternalFile(self):
filename = _tmpDirectory + "/data.h5"
@@ -571,8 +594,6 @@ class TestH5Node(TestCaseQt):
@classmethod
def setUpClass(cls):
super(TestH5Node, cls).setUpClass()
- if h5py is None:
- raise unittest.SkipTest("h5py is not available")
cls.tmpDirectory = tempfile.mkdtemp()
cls.h5Filename = cls.createResource(cls.tmpDirectory)
@@ -809,8 +830,6 @@ class TestHdf5TreeView(TestCaseQt):
def setUp(self):
super(TestHdf5TreeView, self).setUp()
- if h5py is None:
- self.skipTest("h5py is not available")
def testCreate(self):
view = hdf5.Hdf5TreeView()
diff --git a/silx/gui/icons.py b/silx/gui/icons.py
index ef99591..1493b92 100644
--- a/silx/gui/icons.py
+++ b/silx/gui/icons.py
@@ -29,7 +29,7 @@ Use :func:`getQIcon` to create Qt QIcon from the name identifying an icon.
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "05/10/2018"
+__date__ = "07/01/2019"
import os
@@ -213,11 +213,12 @@ class MultiImageAnimatedIcon(AbstractAnimatedIcon):
self.__frames = []
for i in range(100):
try:
- filename = getQFile("%s/%02d" % (filename, i))
+ frame_filename = os.sep.join((filename, ("%02d" %i)))
+ frame_file = getQFile(frame_filename)
except ValueError:
break
try:
- icon = qt.QIcon(filename.fileName())
+ icon = qt.QIcon(frame_file.fileName())
except ValueError:
break
self.__frames.append(icon)
@@ -420,4 +421,5 @@ def getQFile(name):
qfile = qt.QFile(filename)
if qfile.exists():
return qfile
+ _logger.debug("File '%s' not found.", filename)
raise ValueError('Not an icon name: %s' % name)
diff --git a/silx/gui/plot/ColorBar.py b/silx/gui/plot/ColorBar.py
index fd4d34e..9798123 100644
--- a/silx/gui/plot/ColorBar.py
+++ b/silx/gui/plot/ColorBar.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -251,10 +251,13 @@ class ColorBarWidget(qt.QWidget):
def _defaultColormapChanged(self, event):
"""Handle plot default colormap changed"""
- if (event['event'] == 'defaultColormapChanged' and
- self.getPlot().getActiveImage() is None):
- # No active image, take default colormap update into account
- self._syncWithDefaultColormap()
+ if event['event'] == 'defaultColormapChanged':
+ plot = self.getPlot()
+ if (plot is not None and
+ plot.getActiveImage() is None and
+ plot._getActiveItem(kind='scatter') is None):
+ # No active item, take default colormap update into account
+ self._syncWithDefaultColormap()
def _syncWithDefaultColormap(self, data=None):
"""Update colorbar according to plot default colormap"""
@@ -801,7 +804,7 @@ class _TickBar(qt.QWidget):
if self._norm == colors.Colormap.LINEAR:
return 1 - (val - self._vmin) / (self._vmax - self._vmin)
elif self._norm == colors.Colormap.LOGARITHM:
- return 1 - (numpy.log10(val) - numpy.log10(self._vmin)) / (numpy.log10(self._vmax) - numpy.log(self._vmin))
+ return 1 - (numpy.log10(val) - numpy.log10(self._vmin)) / (numpy.log10(self._vmax) - numpy.log10(self._vmin))
else:
raise ValueError('Norm is not recognized')
@@ -864,7 +867,7 @@ class _TickBar(qt.QWidget):
def _guessType(self, font):
"""Try fo find the better format to display the tick's labels
- :param QFont font: the font we want want to use durint the painting
+ :param QFont font: the font we want to use during the painting
"""
form = self._getStandardFormat()
@@ -873,7 +876,7 @@ class _TickBar(qt.QWidget):
for tick in self.ticks:
width = max(fm.width(form.format(tick)), width)
- # if the length of the string are too long we are mooving to scientific
+ # if the length of the string are too long we are moving to scientific
# display
if width > _TickBar._WIDTH_DISP_VAL - _TickBar._LINE_WIDTH:
return self._getScientificForm()
diff --git a/silx/gui/plot/CompareImages.py b/silx/gui/plot/CompareImages.py
index 88b257d..3875be4 100644
--- a/silx/gui/plot/CompareImages.py
+++ b/silx/gui/plot/CompareImages.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2018 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -30,6 +30,7 @@ __license__ = "MIT"
__date__ = "23/07/2018"
+import enum
import logging
import numpy
import weakref
@@ -42,13 +43,16 @@ from silx.gui import plot
from silx.gui import icons
from silx.gui.colors import Colormap
from silx.gui.plot import tools
-from silx.third_party import enum
_logger = logging.getLogger(__name__)
from silx.opencl import ocl
if ocl is not None:
- from silx.opencl import sift
+ try:
+ from silx.opencl import sift
+ except ImportError:
+ # sift module is not available (e.g., in official Debian packages)
+ sift = None
else: # No OpenCL device or no pyopencl
sift = None
@@ -62,6 +66,7 @@ class VisualizationMode(enum.Enum):
HORIZONTAL_LINE = 'hline'
COMPOSITE_RED_BLUE_GRAY = "rbgchannel"
COMPOSITE_RED_BLUE_GRAY_NEG = "rbgnegchannel"
+ COMPOSITE_A_MINUS_B = "aminusb"
@enum.unique
@@ -161,6 +166,16 @@ class CompareImagesToolBar(qt.QToolBar):
self.__ycChannelModeAction = action
self.__visualizationGroup.addAction(action)
+ icon = icons.getQIcon("compare-mode-a-minus-b")
+ action = qt.QAction(icon, "Raw A minus B compare mode", self)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_W))
+ action.setProperty("mode", VisualizationMode.COMPOSITE_A_MINUS_B)
+ menu.addAction(action)
+ self.__ycChannelModeAction = action
+ self.__visualizationGroup.addAction(action)
+
menu = qt.QMenu(self)
self.__alignmentAction = qt.QAction(self)
self.__alignmentAction.setMenu(menu)
@@ -539,6 +554,11 @@ class CompareImages(qt.QMainWindow):
def __init__(self, parent=None, backend=None):
qt.QMainWindow.__init__(self, parent)
+ self._resetZoomActive = True
+ self._colormap = Colormap()
+ """Colormap shared by all modes, except the compose images (rgb image)"""
+ self._colormapKeyPoints = Colormap('spring')
+ """Colormap used for sift keypoints"""
if parent is None:
self.setWindowTitle('Compare images')
@@ -553,6 +573,7 @@ class CompareImages(qt.QMainWindow):
self.__previousSeparatorPosition = None
self.__plot = plot.PlotWidget(parent=self, backend=backend)
+ self.__plot.setDefaultColormap(self._colormap)
self.__plot.getXAxis().setLabel('Columns')
self.__plot.getYAxis().setLabel('Rows')
if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == 'downward':
@@ -630,6 +651,14 @@ class CompareImages(qt.QMainWindow):
"""
return self.__plot
+ def getColormap(self):
+ """
+
+ :return: colormap used for compare image
+ :rtype: silx.gui.colors.Colormap
+ """
+ return self._colormap
+
def getRawPixelData(self, x, y):
"""Return the raw pixel of each image data from axes positions.
@@ -835,7 +864,8 @@ class CompareImages(qt.QMainWindow):
self.__raw1 = image1
self.__raw2 = image2
self.__updateData()
- self.__plot.resetZoom()
+ if self.isAutoResetZoom():
+ self.__plot.resetZoom()
def setImage1(self, image1):
"""Set image1 to be compared.
@@ -850,7 +880,8 @@ class CompareImages(qt.QMainWindow):
"""
self.__raw1 = image1
self.__updateData()
- self.__plot.resetZoom()
+ if self.isAutoResetZoom():
+ self.__plot.resetZoom()
def setImage2(self, image2):
"""Set image2 to be compared.
@@ -865,7 +896,8 @@ class CompareImages(qt.QMainWindow):
"""
self.__raw2 = image2
self.__updateData()
- self.__plot.resetZoom()
+ if self.isAutoResetZoom():
+ self.__plot.resetZoom()
def __updateKeyPoints(self):
"""Update the displayed keypoints using cached keypoints.
@@ -878,11 +910,11 @@ class CompareImages(qt.QMainWindow):
y=data[1],
z=1,
value=data[2],
- legend="keypoints",
- colormap=Colormap("spring"))
+ colormap=self._colormapKeyPoints,
+ legend="keypoints")
def __updateData(self):
- """Compute aligned image when the alignement mode changes.
+ """Compute aligned image when the alignment mode changes.
This function cache input images which are used when
vertical/horizontal separators moves.
@@ -943,6 +975,9 @@ class CompareImages(qt.QMainWindow):
elif mode == VisualizationMode.COMPOSITE_RED_BLUE_GRAY:
data1 = self.__composeImage(data1, data2, mode)
data2 = numpy.empty((0, 0))
+ elif mode == VisualizationMode.COMPOSITE_A_MINUS_B:
+ data1 = self.__composeImage(data1, data2, mode)
+ data2 = numpy.empty((0, 0))
elif mode == VisualizationMode.ONLY_A:
data2 = numpy.empty((0, 0))
elif mode == VisualizationMode.ONLY_B:
@@ -977,7 +1012,8 @@ class CompareImages(qt.QMainWindow):
else:
vmin = min(self.__data1.min(), self.__data2.min())
vmax = max(self.__data1.max(), self.__data2.max())
- colormap = Colormap(vmin=vmin, vmax=vmax)
+ colormap = self.getColormap()
+ colormap.setVRange(vmin=vmin, vmax=vmax)
self.__image1.setColormap(colormap)
self.__image2.setColormap(colormap)
@@ -1025,6 +1061,13 @@ class CompareImages(qt.QMainWindow):
:rtype: numpy.ndarray
"""
assert(data1.shape[0:2] == data2.shape[0:2])
+ if mode == VisualizationMode.COMPOSITE_A_MINUS_B:
+ # TODO: this calculation has no interest of generating a 'composed'
+ # rgb image, this could be moved in an other function or doc
+ # should be modified
+ _type = data1.dtype
+ result = data1.astype(numpy.float64) - data2.astype(numpy.float64)
+ return result
mode1 = self.__getImageMode(data1)
if mode1 in ["rgb", "rgba"]:
intensity1 = self.__luminosityImage(data1)
@@ -1188,3 +1231,19 @@ class CompareImages(qt.QMainWindow):
data2 = result["result"]
self.__transformation = self.__toAffineTransformation(result)
return data1, data2
+
+ def setAutoResetZoom(self, activate=True):
+ """
+
+ :param bool activate: True if we want to activate the automatic
+ plot reset zoom when setting images.
+ """
+ self._resetZoomActive = activate
+
+ def isAutoResetZoom(self):
+ """
+
+ :return: True if the automatic call to resetzoom is activated
+ :rtype: bool
+ """
+ return self._resetZoomActive
diff --git a/silx/gui/plot/ComplexImageView.py b/silx/gui/plot/ComplexImageView.py
index bbcb0a5..c8470ab 100644
--- a/silx/gui/plot/ComplexImageView.py
+++ b/silx/gui/plot/ComplexImageView.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -39,6 +39,7 @@ import logging
import collections
import numpy
+from ...utils.deprecation import deprecated
from .. import qt, icons
from .PlotWindow import Plot2D
from . import items
@@ -170,16 +171,16 @@ class _ComplexDataToolButton(qt.QToolButton):
"""
_MODES = collections.OrderedDict([
- (ImageComplexData.Mode.ABSOLUTE, ('math-amplitude', 'Amplitude')),
- (ImageComplexData.Mode.SQUARE_AMPLITUDE,
+ (ImageComplexData.ComplexMode.ABSOLUTE, ('math-amplitude', 'Amplitude')),
+ (ImageComplexData.ComplexMode.SQUARE_AMPLITUDE,
('math-square-amplitude', 'Square amplitude')),
- (ImageComplexData.Mode.PHASE, ('math-phase', 'Phase')),
- (ImageComplexData.Mode.REAL, ('math-real', 'Real part')),
- (ImageComplexData.Mode.IMAGINARY,
+ (ImageComplexData.ComplexMode.PHASE, ('math-phase', 'Phase')),
+ (ImageComplexData.ComplexMode.REAL, ('math-real', 'Real part')),
+ (ImageComplexData.ComplexMode.IMAGINARY,
('math-imaginary', 'Imaginary part')),
- (ImageComplexData.Mode.AMPLITUDE_PHASE,
+ (ImageComplexData.ComplexMode.AMPLITUDE_PHASE,
('math-phase-color', 'Amplitude and Phase')),
- (ImageComplexData.Mode.LOG10_AMPLITUDE_PHASE,
+ (ImageComplexData.ComplexMode.LOG10_AMPLITUDE_PHASE,
('math-phase-color-log', 'Log10(Amp.) and Phase'))
])
@@ -208,7 +209,7 @@ class _ComplexDataToolButton(qt.QToolButton):
self.setPopupMode(qt.QToolButton.InstantPopup)
- self._modeChanged(self._plot2DComplex.getVisualizationMode())
+ self._modeChanged(self._plot2DComplex.getComplexMode())
self._plot2DComplex.sigVisualizationModeChanged.connect(
self._modeChanged)
@@ -217,7 +218,8 @@ class _ComplexDataToolButton(qt.QToolButton):
icon, text = self._MODES[mode]
self.setIcon(icons.getQIcon(icon))
self.setToolTip('Display the ' + text.lower())
- self._rangeDialogAction.setEnabled(mode == ImageComplexData.Mode.LOG10_AMPLITUDE_PHASE)
+ self._rangeDialogAction.setEnabled(
+ mode == ImageComplexData.ComplexMode.LOG10_AMPLITUDE_PHASE)
def _triggered(self, action):
"""Handle triggering of menu actions"""
@@ -244,8 +246,8 @@ class _ComplexDataToolButton(qt.QToolButton):
else: # update mode
mode = action.data()
- if isinstance(mode, ImageComplexData.Mode):
- self._plot2DComplex.setVisualizationMode(mode)
+ if isinstance(mode, ImageComplexData.ComplexMode):
+ self._plot2DComplex.setComplexMode(mode)
def _rangeChanged(self, range_):
"""Handle updates of range in the dialog"""
@@ -258,8 +260,8 @@ class ComplexImageView(qt.QWidget):
:param parent: See :class:`QMainWindow`
"""
- Mode = ImageComplexData.Mode
- """Also expose the modes inside the class"""
+ ComplexMode = ImageComplexData.ComplexMode
+ """Complex Modes enumeration"""
sigDataChanged = qt.Signal()
"""Signal emitted when data has changed."""
@@ -301,7 +303,7 @@ class ComplexImageView(qt.QWidget):
if event is items.ItemChangedType.DATA:
self.sigDataChanged.emit()
elif event is items.ItemChangedType.VISUALIZATION_MODE:
- mode = self.getVisualizationMode()
+ mode = self.getComplexMode()
self.sigVisualizationModeChanged.emit(mode)
def getPlot(self):
@@ -344,15 +346,34 @@ class ComplexImageView(qt.QWidget):
False to return internal data (do not modify!)
:rtype: numpy.ndarray of float with 2 dims or RGBA image (uint8).
"""
- mode = self.getVisualizationMode()
- if mode in (self.Mode.AMPLITUDE_PHASE,
- self.Mode.LOG10_AMPLITUDE_PHASE):
+ mode = self.getComplexMode()
+ if mode in (self.ComplexMode.AMPLITUDE_PHASE,
+ self.ComplexMode.LOG10_AMPLITUDE_PHASE):
return self._plotImage.getRgbaImageData(copy=copy)
else:
return self._plotImage.getData(copy=copy)
+ # Backward compatibility
+
+ Mode = ComplexMode
+
+ @classmethod
+ @deprecated(replacement='supportedComplexModes', since_version='0.11.0')
+ def getSupportedVisualizationModes(cls):
+ return cls.supportedComplexModes()
+
+ @deprecated(replacement='setComplexMode', since_version='0.11.0')
+ def setVisualizationMode(self, mode):
+ return self.setComplexMode(mode)
+
+ @deprecated(replacement='getComplexMode', since_version='0.11.0')
+ def getVisualizationMode(self):
+ return self.getComplexMode()
+
+ # Image item proxy
+
@staticmethod
- def getSupportedVisualizationModes():
+ def supportedComplexModes():
"""Returns the supported visualization modes.
Supported visualization modes are:
@@ -365,26 +386,33 @@ class ComplexImageView(qt.QWidget):
- log10_amplitude_phase:
Color-coded phase with log10(amplitude) as alpha.
- :rtype: tuple of str
+ :rtype: List[ComplexMode]
"""
- return tuple(ImageComplexData.Mode)
+ return ImageComplexData.supportedComplexModes()
- def setVisualizationMode(self, mode):
+ def setComplexMode(self, mode):
"""Set the mode of visualization of the complex data.
- See :meth:`getSupportedVisualizationModes` for the list of
+ See :meth:`supportedComplexModes` for the list of
supported modes.
- :param str mode: The mode to use.
+ How-to change visualization mode::
+
+ widget = ComplexImageView()
+ widget.setComplexMode(ComplexImageView.ComplexMode.PHASE)
+ # or
+ widget.setComplexMode('phase')
+
+ :param Unions[ComplexMode,str] mode: The mode to use.
"""
- self._plotImage.setVisualizationMode(mode)
+ self._plotImage.setComplexMode(mode)
- def getVisualizationMode(self):
+ def getComplexMode(self):
"""Get the current visualization mode of the complex data.
- :rtype: Mode
+ :rtype: ComplexMode
"""
- return self._plotImage.getVisualizationMode()
+ return self._plotImage.getComplexMode()
def _setAmplitudeRangeInfo(self, max_=None, delta=2):
"""Set the amplitude range to display for 'log10_amplitude_phase' mode.
@@ -402,8 +430,6 @@ class ComplexImageView(qt.QWidget):
:rtype: 2-tuple"""
return self._plotImage._getAmplitudeRangeInfo()
- # Image item proxy
-
def setColormap(self, colormap, mode=None):
"""Set the colormap to use for amplitude, phase, real or imaginary.
@@ -411,14 +437,14 @@ class ComplexImageView(qt.QWidget):
amplitude and phase.
:param ~silx.gui.colors.Colormap colormap: The colormap
- :param Mode mode: If specified, set the colormap of this specific mode
+ :param ComplexMode mode: If specified, set the colormap of this specific mode
"""
self._plotImage.setColormap(colormap, mode)
def getColormap(self, mode=None):
"""Returns the colormap used to display the data.
- :param Mode mode: If specified, set the colormap of this specific mode
+ :param ComplexMode mode: If specified, set the colormap of this specific mode
:rtype: ~silx.gui.colors.Colormap
"""
return self._plotImage.getColormap(mode=mode)
diff --git a/silx/gui/plot/CurvesROIWidget.py b/silx/gui/plot/CurvesROIWidget.py
index 81e684e..050b344 100644
--- a/silx/gui/plot/CurvesROIWidget.py
+++ b/silx/gui/plot/CurvesROIWidget.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -22,50 +22,43 @@
# THE SOFTWARE.
#
# ###########################################################################*/
-"""Widget to handle regions of interest (ROI) on curves displayed in a PlotWindow.
+"""
+Widget to handle regions of interest (:class:`ROI`) on curves displayed in a
+:class:`PlotWindow`.
This widget is meant to work with :class:`PlotWindow`.
-
-ROI are defined by :
-
-- A name (`ROI` column)
-- A type. The type is the label of the x axis.
- This can be used to apply or not some ROI to a curve and do some post processing.
-- The x coordinate of the left limit (`from` column)
-- The x coordinate of the right limit (`to` column)
-- Raw counts: Sum of the curve's values in the defined Region Of Intereset.
-
- .. image:: img/rawCounts.png
-
-- Net counts: Raw counts minus background
-
- .. image:: img/netCounts.png
"""
-__authors__ = ["V.A. Sole", "T. Vincent"]
+__authors__ = ["V.A. Sole", "T. Vincent", "H. Payno"]
__license__ = "MIT"
-__date__ = "13/11/2017"
+__date__ = "13/03/2018"
from collections import OrderedDict
-
import logging
import os
import sys
-import weakref
-
+import functools
import numpy
-
from silx.io import dictdump
from silx.utils import deprecation
-
+from silx.utils.weakref import WeakMethodProxy
from .. import icons, qt
+from silx.gui.plot.items.curve import Curve
+from silx.math.combo import min_max
+import weakref
+from silx.gui.widgets.TableWidget import TableWidget
_logger = logging.getLogger(__name__)
class CurvesROIWidget(qt.QWidget):
- """Widget displaying a table of ROI information.
+ """
+ Widget displaying a table of ROI information.
+
+ Implements also the following behavior:
+
+ * if the roiTable has no ROI when showing create the default ICR one
:param parent: See :class:`QWidget`
:param str name: The title of this widget
@@ -73,16 +66,12 @@ class CurvesROIWidget(qt.QWidget):
sigROIWidgetSignal = qt.Signal(object)
"""Signal of ROIs modifications.
-
- Modification information if given as a dict with an 'event' key
- providing the type of events.
-
- Type of events:
-
- - AddROI, DelROI, LoadROI and ResetROI with keys: 'roilist', 'roidict'
-
- - selectionChanged with keys: 'row', 'col' 'roi', 'key', 'colheader',
- 'rowheader'
+ Modification information if given as a dict with an 'event' key
+ providing the type of events.
+ Type of events:
+ - AddROI, DelROI, LoadROI and ResetROI with keys: 'roilist', 'roidict'
+ - selectionChanged with keys: 'row', 'col' 'roi', 'key', 'colheader',
+ 'rowheader'
"""
sigROISignal = qt.Signal(object)
@@ -91,26 +80,44 @@ class CurvesROIWidget(qt.QWidget):
super(CurvesROIWidget, self).__init__(parent)
if name is not None:
self.setWindowTitle(name)
+ self.__lastSigROISignal = None
+ """Store the last value emitted for the sigRoiSignal. In the case the
+ active curve change we need to add this extra step in order to make
+ sure we won't send twice the sigROISignal.
+ This come from the fact sigROISignal is connected to the
+ activeROIChanged signal which is emitted when raw and net counts
+ values are changing but are not embed in the sigROISignal.
+ """
assert plot is not None
self._plotRef = weakref.ref(plot)
+ self._showAllMarkers = False
+ self.currentROI = None
layout = qt.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
- ##############
+
self.headerLabel = qt.QLabel(self)
self.headerLabel.setAlignment(qt.Qt.AlignHCenter)
self.setHeader()
layout.addWidget(self.headerLabel)
- ##############
- self.roiTable = ROITable(self)
+
+ widgetAllCheckbox = qt.QWidget(parent=self)
+ self._showAllCheckBox = qt.QCheckBox("show all ROI",
+ parent=widgetAllCheckbox)
+ widgetAllCheckbox.setLayout(qt.QHBoxLayout())
+ spacer = qt.QWidget(parent=widgetAllCheckbox)
+ spacer.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed)
+ widgetAllCheckbox.layout().addWidget(spacer)
+ widgetAllCheckbox.layout().addWidget(self._showAllCheckBox)
+ layout.addWidget(widgetAllCheckbox)
+
+ self.roiTable = ROITable(self, plot=plot)
rheight = self.roiTable.horizontalHeader().sizeHint().height()
self.roiTable.setMinimumHeight(4 * rheight)
- self.fillFromROIDict = self.roiTable.fillFromROIDict
- self.getROIListAndDict = self.roiTable.getROIListAndDict
layout.addWidget(self.roiTable)
self._roiFileDir = qt.QDir.home().absolutePath()
- #################
+ self._showAllCheckBox.toggled.connect(self.roiTable.showAllMarkers)
hbox = qt.QWidget(self)
hboxlayout = qt.QHBoxLayout(hbox)
@@ -127,7 +134,8 @@ class CurvesROIWidget(qt.QWidget):
self.addButton.setToolTip('Remove the selected ROI')
self.resetButton = qt.QPushButton(hbox)
self.resetButton.setText("Reset")
- self.addButton.setToolTip('Clear all created ROIs. We only let the default ROI')
+ self.addButton.setToolTip('Clear all created ROIs. We only let the '
+ 'default ROI')
hboxlayout.addWidget(self.addButton)
hboxlayout.addWidget(self.delButton)
@@ -149,19 +157,22 @@ class CurvesROIWidget(qt.QWidget):
layout.addWidget(hbox)
+ # Signal / Slot connections
self.addButton.clicked.connect(self._add)
self.delButton.clicked.connect(self._del)
self.resetButton.clicked.connect(self._reset)
self.loadButton.clicked.connect(self._load)
self.saveButton.clicked.connect(self._save)
- self.roiTable.sigROITableSignal.connect(self._forward)
- self.currentROI = None
- self._middleROIMarkerFlag = False
+ self.roiTable.activeROIChanged.connect(self._emitCurrentROISignal)
+
self._isConnected = False # True if connected to plot signals
self._isInit = False
+ # expose API
+ self.getROIListAndDict = self.roiTable.getROIListAndDict
+
def getPlotWidget(self):
"""Returns the associated PlotWidget or None
@@ -173,10 +184,6 @@ class CurvesROIWidget(qt.QWidget):
self._visibilityChangedHandler(visible=True)
qt.QWidget.showEvent(self, event)
- def hideEvent(self, event):
- self._visibilityChangedHandler(visible=False)
- qt.QWidget.hideEvent(self, event)
-
@property
def roiFileDir(self):
"""The directory from which to load/save ROI from/to files."""
@@ -188,135 +195,81 @@ class CurvesROIWidget(qt.QWidget):
def roiFileDir(self, roiFileDir):
self._roiFileDir = str(roiFileDir)
- def setRois(self, roidict, order=None):
- """Set the ROIs by providing a dictionary of ROI information.
-
- The dictionary keys are the ROI names.
- Each value is a sub-dictionary of ROI info with the following fields:
-
- - ``"from"``: x coordinate of the left limit, as a float
- - ``"to"``: x coordinate of the right limit, as a float
- - ``"type"``: type of ROI, as a string (e.g "channels", "energy")
-
-
- :param roidict: Dictionary of ROIs
- :param str order: Field used for ordering the ROIs.
- One of "from", "to", "type".
- None (default) for no ordering, or same order as specified
- in parameter ``roidict`` if provided as an OrderedDict.
- """
- if order is None or order.lower() == "none":
- roilist = list(roidict.keys())
- else:
- assert order in ["from", "to", "type"]
- roilist = sorted(roidict.keys(),
- key=lambda roi_name: roidict[roi_name].get(order))
-
- return self.roiTable.fillFromROIDict(roilist, roidict)
+ def setRois(self, rois, order=None):
+ return self.roiTable.setRois(rois, order)
def getRois(self, order=None):
- """Return the currently defined ROIs, as an ordered dict.
-
- The dictionary keys are the ROI names.
- Each value is a sub-dictionary of ROI info with the following fields:
-
- - ``"from"``: x coordinate of the left limit, as a float
- - ``"to"``: x coordinate of the right limit, as a float
- - ``"type"``: type of ROI, as a string (e.g "channels", "energy")
-
-
- :param order: Field used for ordering the ROIs.
- One of "from", "to", "type", "netcounts", "rawcounts".
- None (default) to get the same order as displayed in the widget.
- :return: Ordered dictionary of ROI information
- """
- roilist, roidict = self.roiTable.getROIListAndDict()
- if order is None or order.lower() == "none":
- ordered_roilist = roilist
- else:
- assert order in ["from", "to", "type", "netcounts", "rawcounts"]
- ordered_roilist = sorted(roidict.keys(),
- key=lambda roi_name: roidict[roi_name].get(order))
-
- return OrderedDict([(name, roidict[name]) for name in ordered_roilist])
+ return self.roiTable.getRois(order)
def setMiddleROIMarkerFlag(self, flag=True):
- """Activate or deactivate middle marker.
-
- This allows shifting both min and max limits at once, by dragging
- a marker located in the middle.
-
- :param bool flag: True to activate middle ROI marker
- """
- if flag:
- self._middleROIMarkerFlag = True
- else:
- self._middleROIMarkerFlag = False
+ return self.roiTable.setMiddleROIMarkerFlag(flag)
def _add(self):
"""Add button clicked handler"""
+ def getNextRoiName():
+ rois = self.roiTable.getRois(order=None)
+ roisNames = []
+ [roisNames.append(roiName) for roiName in rois]
+ nrois = len(rois)
+ if nrois == 0:
+ return "ICR"
+ else:
+ i = 1
+ newroi = "newroi %d" % i
+ while newroi in roisNames:
+ i += 1
+ newroi = "newroi %d" % i
+ return newroi
+ roi = ROI(name=getNextRoiName())
+
+ if roi.getName() == "ICR":
+ roi.setType("Default")
+ else:
+ roi.setType(self.getPlotWidget().getXAxis().getLabel())
+
+ xmin, xmax = self.getPlotWidget().getXAxis().getLimits()
+ fromdata = xmin + 0.25 * (xmax - xmin)
+ todata = xmin + 0.75 * (xmax - xmin)
+ if roi.isICR():
+ fromdata, dummy0, todata, dummy1 = self._getAllLimits()
+ roi.setFrom(fromdata)
+ roi.setTo(todata)
+ self.roiTable.addRoi(roi)
+
+ # back compatibility pymca roi signals
ddict = {}
ddict['event'] = "AddROI"
- roilist, roidict = self.roiTable.getROIListAndDict()
- ddict['roilist'] = roilist
- ddict['roidict'] = roidict
+ ddict['roilist'] = self.roiTable.roidict.values()
+ ddict['roidict'] = self.roiTable.roidict
self.sigROIWidgetSignal.emit(ddict)
+ # end back compatibility pymca roi signals
def _del(self):
"""Delete button clicked handler"""
- row = self.roiTable.currentRow()
- if row >= 0:
- index = self.roiTable.labels.index('Type')
- text = str(self.roiTable.item(row, index).text())
- if text.upper() != 'DEFAULT':
- index = self.roiTable.labels.index('ROI')
- key = str(self.roiTable.item(row, index).text())
- else:
- # This is to prevent deleting ICR ROI, that is
- # usually initialized as "Default" type.
- return
- roilist, roidict = self.roiTable.getROIListAndDict()
- row = roilist.index(key)
- del roilist[row]
- del roidict[key]
- if len(roilist) > 0:
- currentroi = roilist[0]
- else:
- currentroi = None
-
- self.roiTable.fillFromROIDict(roilist=roilist,
- roidict=roidict,
- currentroi=currentroi)
- ddict = {}
- ddict['event'] = "DelROI"
- ddict['roilist'] = roilist
- ddict['roidict'] = roidict
- self.sigROIWidgetSignal.emit(ddict)
-
- def _forward(self, ddict):
- """Broadcast events from ROITable signal"""
+ self.roiTable.deleteActiveRoi()
+
+ # back compatibility pymca roi signals
+ ddict = {}
+ ddict['event'] = "DelROI"
+ ddict['roilist'] = self.roiTable.roidict.values()
+ ddict['roidict'] = self.roiTable.roidict
self.sigROIWidgetSignal.emit(ddict)
+ # end back compatibility pymca roi signals
def _reset(self):
"""Reset button clicked handler"""
+ self.roiTable.clear()
+ old = self.blockSignals(True) # avoid several sigROISignal emission
+ self._add()
+ self.blockSignals(old)
+
+ # back compatibility pymca roi signals
ddict = {}
ddict['event'] = "ResetROI"
- roilist0, roidict0 = self.roiTable.getROIListAndDict()
- index = 0
- for key in roilist0:
- if roidict0[key]['type'].upper() == 'DEFAULT':
- index = roilist0.index(key)
- break
- roilist = []
- roidict = {}
- if len(roilist0):
- roilist.append(roilist0[index])
- roidict[roilist[0]] = {}
- roidict[roilist[0]].update(roidict0[roilist[0]])
- self.roiTable.fillFromROIDict(roilist=roilist, roidict=roidict)
- ddict['roilist'] = roilist
- ddict['roidict'] = roidict
+ ddict['roilist'] = self.roiTable.roidict.values()
+ ddict['roidict'] = self.roiTable.roidict
self.sigROIWidgetSignal.emit(ddict)
+ # end back compatibility pymca roi signals
def _load(self):
"""Load button clicked handler"""
@@ -334,32 +287,22 @@ class CurvesROIWidget(qt.QWidget):
dialog.close()
self.roiFileDir = os.path.dirname(outputFile)
- self.load(outputFile)
+ self.roiTable.load(outputFile)
+
+ # back compatibility pymca roi signals
+ ddict = {}
+ ddict['event'] = "LoadROI"
+ ddict['roilist'] = self.roiTable.roidict.values()
+ ddict['roidict'] = self.roiTable.roidict
+ self.sigROIWidgetSignal.emit(ddict)
+ # end back compatibility pymca roi signals
def load(self, filename):
"""Load ROI widget information from a file storing a dict of ROI.
:param str filename: The file from which to load ROI
"""
- rois = dictdump.load(filename)
- currentROI = None
- if self.roiTable.rowCount():
- item = self.roiTable.item(self.roiTable.currentRow(), 0)
- if item is not None:
- currentROI = str(item.text())
-
- # Remove rawcounts and netcounts from ROIs
- for roi in rois['ROI']['roidict'].values():
- roi.pop('rawcounts', None)
- roi.pop('netcounts', None)
-
- self.roiTable.fillFromROIDict(roilist=rois['ROI']['roilist'],
- roidict=rois['ROI']['roidict'],
- currentroi=currentROI)
-
- roilist, roidict = self.roiTable.getROIListAndDict()
- event = {'event': 'LoadROI', 'roilist': roilist, 'roidict': roidict}
- self.sigROIWidgetSignal.emit(event)
+ self.roiTable.load(filename)
def _save(self):
"""Save button clicked handler"""
@@ -396,142 +339,24 @@ class CurvesROIWidget(qt.QWidget):
:param str filename: The file to which to save the ROIs
"""
- roilist, roidict = self.roiTable.getROIListAndDict()
- datadict = {'ROI': {'roilist': roilist, 'roidict': roidict}}
- dictdump.dump(datadict, filename)
+ self.roiTable.save(filename)
def setHeader(self, text='ROIs'):
"""Set the header text of this widget"""
self.headerLabel.setText("<b>%s<\b>" % text)
- def _roiSignal(self, ddict):
- """Handle ROI widget signal"""
- _logger.debug("CurvesROIWidget._roiSignal %s", str(ddict))
- plot = self.getPlotWidget()
- if plot is None:
- return
-
- if ddict['event'] == "AddROI":
- xmin, xmax = plot.getXAxis().getLimits()
- fromdata = xmin + 0.25 * (xmax - xmin)
- todata = xmin + 0.75 * (xmax - xmin)
- plot.remove('ROI min', kind='marker')
- plot.remove('ROI max', kind='marker')
- if self._middleROIMarkerFlag:
- plot.remove('ROI middle', kind='marker')
- roiList, roiDict = self.roiTable.getROIListAndDict()
- nrois = len(roiList)
- if nrois == 0:
- newroi = "ICR"
- fromdata, dummy0, todata, dummy1 = self._getAllLimits()
- draggable = False
- color = 'black'
- else:
- # find the next index free for newroi.
- for i in range(nrois):
- i += 1
- newroi = "newroi %d" % i
- if newroi not in roiList:
- break
- color = 'blue'
- draggable = True
- plot.addXMarker(fromdata,
- legend='ROI min',
- text='ROI min',
- color=color,
- draggable=draggable)
- plot.addXMarker(todata,
- legend='ROI max',
- text='ROI max',
- color=color,
- draggable=draggable)
- if draggable and self._middleROIMarkerFlag:
- pos = 0.5 * (fromdata + todata)
- plot.addXMarker(pos,
- legend='ROI middle',
- text="",
- color='yellow',
- draggable=draggable)
- roiList.append(newroi)
- roiDict[newroi] = {}
- if newroi == "ICR":
- roiDict[newroi]['type'] = "Default"
- else:
- roiDict[newroi]['type'] = plot.getXAxis().getLabel()
- roiDict[newroi]['from'] = fromdata
- roiDict[newroi]['to'] = todata
- self.roiTable.fillFromROIDict(roilist=roiList,
- roidict=roiDict,
- currentroi=newroi)
- self.currentROI = newroi
- self.calculateRois()
- elif ddict['event'] in ['DelROI', "ResetROI"]:
- plot.remove('ROI min', kind='marker')
- plot.remove('ROI max', kind='marker')
- if self._middleROIMarkerFlag:
- plot.remove('ROI middle', kind='marker')
- roiList, roiDict = self.roiTable.getROIListAndDict()
- roiDictKeys = list(roiDict.keys())
- if len(roiDictKeys):
- currentroi = roiDictKeys[0]
- else:
- # create again the ICR
- ddict = {"event": "AddROI"}
- return self._roiSignal(ddict)
-
- self.roiTable.fillFromROIDict(roilist=roiList,
- roidict=roiDict,
- currentroi=currentroi)
- self.currentROI = currentroi
-
- elif ddict['event'] == 'LoadROI':
- self.calculateRois()
+ @deprecation.deprecated(replacement="calculateRois",
+ reason="CamelCase convention",
+ since_version="0.7")
+ def calculateROIs(self, *args, **kw):
+ self.calculateRois(*args, **kw)
- elif ddict['event'] == 'selectionChanged':
- _logger.debug("Selection changed")
- self.roilist, self.roidict = self.roiTable.getROIListAndDict()
- fromdata = ddict['roi']['from']
- todata = ddict['roi']['to']
- plot.remove('ROI min', kind='marker')
- plot.remove('ROI max', kind='marker')
- if ddict['key'] == 'ICR':
- draggable = False
- color = 'black'
- else:
- draggable = True
- color = 'blue'
- plot.addXMarker(fromdata,
- legend='ROI min',
- text='ROI min',
- color=color,
- draggable=draggable)
- plot.addXMarker(todata,
- legend='ROI max',
- text='ROI max',
- color=color,
- draggable=draggable)
- if draggable and self._middleROIMarkerFlag:
- pos = 0.5 * (fromdata + todata)
- plot.addXMarker(pos,
- legend='ROI middle',
- text="",
- color='yellow',
- draggable=True)
- self.currentROI = ddict['key']
- if ddict['colheader'] in ['From', 'To']:
- dict0 = {}
- dict0['event'] = "SetActiveCurveEvent"
- dict0['legend'] = plot.getActiveCurve(just_legend=1)
- plot.setActiveCurve(dict0['legend'])
- elif ddict['colheader'] == 'Raw Counts':
- pass
- elif ddict['colheader'] == 'Net Counts':
- pass
- else:
- self._emitCurrentROISignal()
+ def calculateRois(self, roiList=None, roiDict=None):
+ """Compute ROI information"""
+ return self.roiTable.calculateRois()
- else:
- _logger.debug("Unknown or ignored event %s", ddict['event'])
+ def showAllMarkers(self, _show=True):
+ self.roiTable.showAllMarkers(_show)
def _getAllLimits(self):
"""Retrieve the limits based on the curves."""
@@ -565,429 +390,1137 @@ class CurvesROIWidget(qt.QWidget):
return xmin, ymin, xmax, ymax
- @deprecation.deprecated(replacement="calculateRois",
- reason="CamelCase convention")
- def calculateROIs(self, *args, **kw):
- self.calculateRois(*args, **kw)
+ def showEvent(self, event):
+ self._visibilityChangedHandler(visible=True)
+ qt.QWidget.showEvent(self, event)
- def calculateRois(self, roiList=None, roiDict=None):
- """Compute ROI information"""
- if roiList is None or roiDict is None:
- roiList, roiDict = self.roiTable.getROIListAndDict()
+ def hideEvent(self, event):
+ self._visibilityChangedHandler(visible=False)
+ qt.QWidget.hideEvent(self, event)
- plot = self.getPlotWidget()
- if plot is None:
- activeCurve = None
+ def _visibilityChangedHandler(self, visible):
+ """Handle widget's visibility updates.
+
+ It is connected to plot signals only when visible.
+ """
+ if visible:
+ # if no ROI existing yet, add the default one
+ if self.roiTable.rowCount() is 0:
+ old = self.blockSignals(True) # avoid several sigROISignal emission
+ self._add()
+ self.blockSignals(old)
+ self.calculateRois()
+
+ def fillFromROIDict(self, *args, **kwargs):
+ self.roiTable.fillFromROIDict(*args, **kwargs)
+
+ def _emitCurrentROISignal(self):
+ ddict = {}
+ ddict['event'] = "currentROISignal"
+ if self.roiTable.activeRoi is not None:
+ ddict['ROI'] = self.roiTable.activeRoi.toDict()
+ ddict['current'] = self.roiTable.activeRoi.getName()
+ else:
+ ddict['current'] = None
+
+ if self.__lastSigROISignal != ddict:
+ self.__lastSigROISignal = ddict
+ self.sigROISignal.emit(ddict)
+
+ @property
+ def currentRoi(self):
+ return self.roiTable.activeRoi
+
+
+class _FloatItem(qt.QTableWidgetItem):
+ """
+ Simple QTableWidgetItem overloading the < operator to deal with ordering
+ """
+ def __init__(self):
+ qt.QTableWidgetItem.__init__(self, type=qt.QTableWidgetItem.Type)
+
+ def __lt__(self, other):
+ if self.text() in ('', ROITable.INFO_NOT_FOUND):
+ return False
+ if other.text() in ('', ROITable.INFO_NOT_FOUND):
+ return True
+ return float(self.text()) < float(other.text())
+
+
+class ROITable(TableWidget):
+ """Table widget displaying ROI information.
+
+ See :class:`QTableWidget` for constructor arguments.
+
+ Behavior: listen at the active curve changed only when the widget is
+ visible. Otherwise won't compute the row and net counts...
+ """
+
+ activeROIChanged = qt.Signal()
+ """Signal emitted when the active roi changed or when the value of the
+ active roi are changing"""
+
+ COLUMNS_INDEX = OrderedDict([
+ ('ID', 0),
+ ('ROI', 1),
+ ('Type', 2),
+ ('From', 3),
+ ('To', 4),
+ ('Raw Counts', 5),
+ ('Net Counts', 6),
+ ('Raw Area', 7),
+ ('Net Area', 8),
+ ])
+
+ COLUMNS = list(COLUMNS_INDEX.keys())
+
+ INFO_NOT_FOUND = '????????'
+
+ def __init__(self, parent=None, plot=None, rois=None):
+ super(ROITable, self).__init__(parent)
+ self._showAllMarkers = False
+ self._userIsEditingRoi = False
+ """bool used to avoid conflict when editing the ROI object"""
+ self._isConnected = False
+ self._roiToItems = {}
+ self._roiDict = {}
+ """dict of ROI object. Key is ROi id, value is the ROI object"""
+ self._markersHandler = _RoiMarkerManager()
+
+ """
+ Associate for each marker legend used when the `_showAllMarkers` option
+ is active a roi.
+ """
+ self.setColumnCount(len(self.COLUMNS))
+ self.setPlot(plot)
+ self.__setTooltip()
+ self.setSortingEnabled(True)
+ self.itemChanged.connect(self._itemChanged)
+
+ @property
+ def roidict(self):
+ return self._getRoiDict()
+
+ @property
+ def activeRoi(self):
+ return self._markersHandler._activeRoi
+
+ def _getRoiDict(self):
+ ddict = {}
+ for id in self._roiDict:
+ ddict[self._roiDict[id].getName()] = self._roiDict[id]
+ return ddict
+
+ def clear(self):
+ """
+ .. note:: clear the interface only. keep the roidict...
+ """
+ self._markersHandler.clear()
+ self._roiToItems = {}
+ self._roiDict = {}
+
+ qt.QTableWidget.clear(self)
+ self.setRowCount(0)
+ self.setHorizontalHeaderLabels(self.COLUMNS)
+ header = self.horizontalHeader()
+ if hasattr(header, 'setSectionResizeMode'): # Qt5
+ header.setSectionResizeMode(qt.QHeaderView.ResizeToContents)
+ else: # Qt4
+ header.setResizeMode(qt.QHeaderView.ResizeToContents)
+ self.sortByColumn(0, qt.Qt.AscendingOrder)
+ self.hideColumn(self.COLUMNS_INDEX['ID'])
+
+ def setPlot(self, plot):
+ self.clear()
+ self.plot = plot
+
+ def __setTooltip(self):
+ self.horizontalHeaderItem(self.COLUMNS_INDEX['ROI']).setToolTip(
+ 'Region of interest identifier')
+ self.horizontalHeaderItem(self.COLUMNS_INDEX['Type']).setToolTip(
+ 'Type of the ROI')
+ self.horizontalHeaderItem(self.COLUMNS_INDEX['From']).setToolTip(
+ 'X-value of the min point')
+ self.horizontalHeaderItem(self.COLUMNS_INDEX['To']).setToolTip(
+ 'X-value of the max point')
+ self.horizontalHeaderItem(self.COLUMNS_INDEX['Raw Counts']).setToolTip(
+ 'Estimation of the integral between y=0 and the selected curve')
+ self.horizontalHeaderItem(self.COLUMNS_INDEX['Net Counts']).setToolTip(
+ 'Estimation of the integral between the segment [maxPt, minPt] '
+ 'and the selected curve')
+
+ def setRois(self, rois, order=None):
+ """Set the ROIs by providing a dictionary of ROI information.
+
+ The dictionary keys are the ROI names.
+ Each value is a sub-dictionary of ROI info with the following fields:
+
+ - ``"from"``: x coordinate of the left limit, as a float
+ - ``"to"``: x coordinate of the right limit, as a float
+ - ``"type"``: type of ROI, as a string (e.g "channels", "energy")
+
+
+ :param roidict: Dictionary of ROIs
+ :param str order: Field used for ordering the ROIs.
+ One of "from", "to", "type".
+ None (default) for no ordering, or same order as specified
+ in parameter ``roidict`` if provided as an OrderedDict.
+ """
+ assert order in [None, "from", "to", "type"]
+ self.clear()
+
+ # backward compatibility since 0.10.0
+ if isinstance(rois, dict):
+ for roiName, roi in rois.items():
+ if isinstance(roi, ROI):
+ _roi = roi
+ else:
+ roi['name'] = roiName
+ _roi = ROI._fromDict(roi)
+ self.addRoi(_roi)
else:
- activeCurve = plot.getActiveCurve(just_legend=False)
+ for roi in rois:
+ assert isinstance(roi, ROI)
+ self.addRoi(roi)
+ self._updateMarkers()
- if activeCurve is None:
- xproc = None
- yproc = None
- self.setHeader()
+ def addRoi(self, roi):
+ """
+
+ :param :class:`ROI` roi: roi to add to the table
+ """
+ assert isinstance(roi, ROI)
+ self._getItem(name='ID', row=None, roi=roi)
+ self._roiDict[roi.getID()] = roi
+ self._markersHandler.add(roi, _RoiMarkerHandler(roi, self.plot))
+ self._updateRoiInfo(roi.getID())
+ callback = functools.partial(WeakMethodProxy(self._updateRoiInfo),
+ roi.getID())
+ roi.sigChanged.connect(callback)
+ # set it as the active one
+ self.setActiveRoi(roi)
+
+ def _getItem(self, name, row, roi):
+ if row:
+ item = self.item(row, self.COLUMNS_INDEX[name])
else:
- x = activeCurve.getXData(copy=False)
- y = activeCurve.getYData(copy=False)
- legend = activeCurve.getLegend()
- idx = numpy.argsort(x, kind='mergesort')
- xproc = numpy.take(x, idx)
- yproc = numpy.take(y, idx)
- self.setHeader('ROIs of %s' % legend)
-
- for key in roiList:
- if key == 'ICR':
- if xproc is not None:
- roiDict[key]['from'] = xproc.min()
- roiDict[key]['to'] = xproc.max()
+ item = None
+ if item:
+ return item
+ else:
+ if name == 'ID':
+ assert roi
+ if roi.getID() in self._roiToItems:
+ return self._roiToItems[roi.getID()]
+ else:
+ # create a new row
+ row = self.rowCount()
+ self.setRowCount(self.rowCount() + 1)
+ item = qt.QTableWidgetItem(str(roi.getID()),
+ type=qt.QTableWidgetItem.Type)
+ self._roiToItems[roi.getID()] = item
+ elif name == 'ROI':
+ item = qt.QTableWidgetItem(roi.getName() if roi else '',
+ type=qt.QTableWidgetItem.Type)
+ if roi.getName().upper() in ('ICR', 'DEFAULT'):
+ item.setFlags(qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled)
else:
- roiDict[key]['from'] = 0
- roiDict[key]['to'] = -1
- fromData = roiDict[key]['from']
- toData = roiDict[key]['to']
- if xproc is not None:
- idx = numpy.nonzero((fromData <= xproc) &
- (xproc <= toData))[0]
- if len(idx):
- xw = xproc[idx]
- yw = yproc[idx]
- rawCounts = yw.sum(dtype=numpy.float)
- deltaX = xw[-1] - xw[0]
- deltaY = yw[-1] - yw[0]
- if deltaX > 0.0:
- slope = (deltaY / deltaX)
- background = yw[0] + slope * (xw - xw[0])
- netCounts = (rawCounts -
- background.sum(dtype=numpy.float))
- else:
- netCounts = 0.0
+ item.setFlags(qt.Qt.ItemIsSelectable |
+ qt.Qt.ItemIsEnabled |
+ qt.Qt.ItemIsEditable)
+ elif name == 'Type':
+ item = qt.QTableWidgetItem(type=qt.QTableWidgetItem.Type)
+ item.setFlags((qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled))
+ elif name in ('To', 'From'):
+ item = _FloatItem()
+ if roi.getName().upper() in ('ICR', 'DEFAULT'):
+ item.setFlags(qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled)
else:
- rawCounts = 0.0
- netCounts = 0.0
- roiDict[key]['rawcounts'] = rawCounts
- roiDict[key]['netcounts'] = netCounts
+ item.setFlags(qt.Qt.ItemIsSelectable |
+ qt.Qt.ItemIsEnabled |
+ qt.Qt.ItemIsEditable)
+ elif name in ('Raw Counts', 'Net Counts', 'Raw Area', 'Net Area'):
+ item = _FloatItem()
+ item.setFlags((qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled))
else:
- roiDict[key].pop('rawcounts', None)
- roiDict[key].pop('netcounts', None)
+ raise ValueError('item type not recognized')
+
+ self.setItem(row, self.COLUMNS_INDEX[name], item)
+ return item
+
+ def _itemChanged(self, item):
+ def getRoi():
+ IDItem = self.item(item.row(), self.COLUMNS_INDEX['ID'])
+ assert IDItem
+ id = int(IDItem.text())
+ assert id in self._roiDict
+ roi = self._roiDict[id]
+ return roi
+
+ def signalChanged(roi):
+ if self.activeRoi and roi.getID() == self.activeRoi.getID():
+ self.activeROIChanged.emit()
+
+ self._userIsEditingRoi = True
+ if item.column() in (self.COLUMNS_INDEX['To'], self.COLUMNS_INDEX['From']):
+ roi = getRoi()
+
+ if item.text() not in ('', self.INFO_NOT_FOUND):
+ try:
+ value = float(item.text())
+ except ValueError:
+ value = 0
+ changed = False
+ if item.column() == self.COLUMNS_INDEX['To']:
+ if value != roi.getTo():
+ roi.setTo(value)
+ changed = True
+ else:
+ assert(item.column() == self.COLUMNS_INDEX['From'])
+ if value != roi.getFrom():
+ roi.setFrom(value)
+ changed = True
+ if changed:
+ self._updateMarker(roi.getName())
+ signalChanged(roi)
+
+ if item.column() is self.COLUMNS_INDEX['ROI']:
+ roi = getRoi()
+ if roi.getName() != item.text():
+ roi.setName(item.text())
+ self._markersHandler.getMarkerHandler(roi.getID()).updateTexts()
+ signalChanged(roi)
+
+ self._userIsEditingRoi = False
+
+ def deleteActiveRoi(self):
+ """
+ remove the current active roi
+ """
+ activeItems = self.selectedItems()
+ if len(activeItems) is 0:
+ return
+ old = self.blockSignals(True) # avoid several emission of sigROISignal
+ roiToRm = set()
+ for item in activeItems:
+ row = item.row()
+ itemID = self.item(row, self.COLUMNS_INDEX['ID'])
+ roiToRm.add(self._roiDict[int(itemID.text())])
+ [self.removeROI(roi) for roi in roiToRm]
+ self.blockSignals(old)
+ self.setActiveRoi(None)
+
+ def removeROI(self, roi):
+ """
+ remove the requested roi
- self.roiTable.fillFromROIDict(
- roilist=roiList,
- roidict=roiDict,
- currentroi=self.currentROI if self.currentROI in roiList else None)
+ :param str name: the name of the roi to remove from the table
+ """
+ if roi and roi.getID() in self._roiToItems:
+ item = self._roiToItems[roi.getID()]
+ self.removeRow(item.row())
+ del self._roiToItems[roi.getID()]
- def _emitCurrentROISignal(self):
- ddict = {}
- ddict['event'] = "currentROISignal"
- _roiList, roiDict = self.roiTable.getROIListAndDict()
- if self.currentROI in roiDict:
- ddict['ROI'] = roiDict[self.currentROI]
+ assert roi.getID() in self._roiDict
+ del self._roiDict[roi.getID()]
+ self._markersHandler.remove(roi)
+
+ callback = functools.partial(WeakMethodProxy(self._updateRoiInfo),
+ roi.getID())
+ roi.sigChanged.connect(callback)
+
+ def setActiveRoi(self, roi):
+ """
+ Define the given roi as the active one.
+
+ .. warning:: this roi should already be registred / added to the table
+
+ :param :class:`ROI` roi: the roi to defined as active
+ """
+ if roi is None:
+ self.clearSelection()
+ self._markersHandler.setActiveRoi(None)
+ self.activeROIChanged.emit()
else:
- self.currentROI = None
- ddict['current'] = self.currentROI
- self.sigROISignal.emit(ddict)
+ assert isinstance(roi, ROI)
+ if roi and roi.getID() in self._roiToItems.keys():
+ # avoid several call back to setActiveROI
+ old = self.blockSignals(True)
+ self.selectRow(self._roiToItems[roi.getID()].row())
+ self.blockSignals(old)
+ self._markersHandler.setActiveRoi(roi)
+ self.activeROIChanged.emit()
+
+ def _updateRoiInfo(self, roiID):
+ if self._userIsEditingRoi is True:
+ return
+ if roiID not in self._roiDict:
+ return
+ roi = self._roiDict[roiID]
+ if roi.isICR():
+ activeCurve = self.plot.getActiveCurve()
+ if activeCurve:
+ xData = activeCurve.getXData()
+ if len(xData) > 0:
+ min, max = min_max(xData)
+ roi.blockSignals(True)
+ roi.setFrom(min)
+ roi.setTo(max)
+ roi.blockSignals(False)
+
+ itemID = self._getItem(name='ID', roi=roi, row=None)
+ itemName = self._getItem(name='ROI', row=itemID.row(), roi=roi)
+ itemName.setText(roi.getName())
+
+ itemType = self._getItem(name='Type', row=itemID.row(), roi=roi)
+ itemType.setText(roi.getType() or self.INFO_NOT_FOUND)
+
+ itemFrom = self._getItem(name='From', row=itemID.row(), roi=roi)
+ fromdata = str(roi.getFrom()) if roi.getFrom() is not None else self.INFO_NOT_FOUND
+ itemFrom.setText(fromdata)
+
+ itemTo = self._getItem(name='To', row=itemID.row(), roi=roi)
+ todata = str(roi.getTo()) if roi.getTo() is not None else self.INFO_NOT_FOUND
+ itemTo.setText(todata)
+
+ rawCounts, netCounts = roi.computeRawAndNetCounts(
+ curve=self.plot.getActiveCurve(just_legend=False))
+ itemRawCounts = self._getItem(name='Raw Counts', row=itemID.row(),
+ roi=roi)
+ rawCounts = str(rawCounts) if rawCounts is not None else self.INFO_NOT_FOUND
+ itemRawCounts.setText(rawCounts)
+
+ itemNetCounts = self._getItem(name='Net Counts', row=itemID.row(),
+ roi=roi)
+ netCounts = str(netCounts) if netCounts is not None else self.INFO_NOT_FOUND
+ itemNetCounts.setText(netCounts)
+
+ rawArea, netArea = roi.computeRawAndNetArea(
+ curve=self.plot.getActiveCurve(just_legend=False))
+ itemRawArea = self._getItem(name='Raw Area', row=itemID.row(),
+ roi=roi)
+ rawArea = str(rawArea) if rawArea is not None else self.INFO_NOT_FOUND
+ itemRawArea.setText(rawArea)
+
+ itemNetArea = self._getItem(name='Net Area', row=itemID.row(),
+ roi=roi)
+ netArea = str(netArea) if netArea is not None else self.INFO_NOT_FOUND
+ itemNetArea.setText(netArea)
+
+ if self.activeRoi and roi.getID() == self.activeRoi.getID():
+ self.activeROIChanged.emit()
+
+ def currentChanged(self, current, previous):
+ if previous and current.row() != previous.row() and current.row() >= 0:
+ roiItem = self.item(current.row(),
+ self.COLUMNS_INDEX['ID'])
+
+ assert roiItem
+ self.setActiveRoi(self._roiDict[int(roiItem.text())])
+ self._markersHandler.updateAllMarkers()
+ qt.QTableWidget.currentChanged(self, current, previous)
+
+ @deprecation.deprecated(reason="Removed",
+ replacement="roidict and roidict.values()",
+ since_version="0.10.0")
+ def getROIListAndDict(self):
+ """
- def _handleROIMarkerEvent(self, ddict):
- """Handle plot signals related to marker events."""
- if ddict['event'] == 'markerMoved':
+ :return: the list of roi objects and the dictionary of roi name to roi
+ object.
+ """
+ roidict = self._roiDict
+ return list(roidict.values()), roidict
- label = ddict['label']
- if label not in ['ROI min', 'ROI max', 'ROI middle']:
- return
+ def calculateRois(self, roiList=None, roiDict=None):
+ """
+ Update values of all registred rois (raw and net counts in particular)
- roiList, roiDict = self.roiTable.getROIListAndDict()
- if self.currentROI is None:
- return
- if self.currentROI not in roiDict:
+ :param roiList: deprecated parameter
+ :param roiDict: deprecated parameter
+ """
+ if roiDict:
+ deprecation.deprecated_warning(name='roiDict', type_='Parameter',
+ reason='Unused parameter',
+ since_version="0.10.0")
+ if roiList:
+ deprecation.deprecated_warning(name='roiList', type_='Parameter',
+ reason='Unused parameter',
+ since_version="0.10.0")
+
+ for roiID in self._roiDict:
+ self._updateRoiInfo(roiID)
+
+ def _updateMarker(self, roiID):
+ """Make sure the marker of the given roi name is updated"""
+ if self._showAllMarkers or (self.activeRoi
+ and self.activeRoi.getName() == roiID):
+ self._updateMarkers()
+
+ def _updateMarkers(self):
+ if self._showAllMarkers is True:
+ self._markersHandler.updateMarkers()
+ else:
+ if not self.activeRoi or not self.plot:
return
+ assert isinstance(self.activeRoi, ROI)
+ markerHandler = self._markersHandler.getMarkerHandler(self.activeRoi.getID())
+ if markerHandler is not None:
+ markerHandler.updateMarkers()
- plot = self.getPlotWidget()
- if plot is None:
- return
+ def getRois(self, order):
+ """
+ Return the currently defined ROIs, as an ordered dict.
- x = ddict['x']
-
- if label == 'ROI min':
- roiDict[self.currentROI]['from'] = x
- if self._middleROIMarkerFlag:
- pos = 0.5 * (roiDict[self.currentROI]['to'] +
- roiDict[self.currentROI]['from'])
- plot.addXMarker(pos,
- legend='ROI middle',
- text='',
- color='yellow',
- draggable=True)
- elif label == 'ROI max':
- roiDict[self.currentROI]['to'] = x
- if self._middleROIMarkerFlag:
- pos = 0.5 * (roiDict[self.currentROI]['to'] +
- roiDict[self.currentROI]['from'])
- plot.addXMarker(pos,
- legend='ROI middle',
- text='',
- color='yellow',
- draggable=True)
- elif label == 'ROI middle':
- delta = x - 0.5 * (roiDict[self.currentROI]['from'] +
- roiDict[self.currentROI]['to'])
- roiDict[self.currentROI]['from'] += delta
- roiDict[self.currentROI]['to'] += delta
- plot.addXMarker(roiDict[self.currentROI]['from'],
- legend='ROI min',
- text='ROI min',
- color='blue',
- draggable=True)
- plot.addXMarker(roiDict[self.currentROI]['to'],
- legend='ROI max',
- text='ROI max',
- color='blue',
- draggable=True)
- else:
- return
- self.calculateRois(roiList, roiDict)
- self._emitCurrentROISignal()
+ The dictionary keys are the ROI names.
+ Each value is a :class:`ROI` object..
- def _visibilityChangedHandler(self, visible):
- """Handle widget's visibility updates.
+ :param order: Field used for ordering the ROIs.
+ One of "from", "to", "type", "netcounts", "rawcounts".
+ None (default) to get the same order as displayed in the widget.
+ :return: Ordered dictionary of ROI information
+ """
- It is connected to plot signals only when visible.
+ if order is None or order.lower() == "none":
+ ordered_roilist = list(self._roiDict.values())
+ res = OrderedDict([(roi.getName(), self._roiDict[roi.getID()]) for roi in ordered_roilist])
+ else:
+ assert order in ["from", "to", "type", "netcounts", "rawcounts"]
+ ordered_roilist = sorted(self._roiDict.keys(),
+ key=lambda roi_id: self._roiDict[roi_id].get(order))
+ res = OrderedDict([(roi.getName(), self._roiDict[id]) for id in ordered_roilist])
+
+ return res
+
+ def save(self, filename):
"""
- plot = self.getPlotWidget()
+ Save current ROIs of the widget as a dict of ROI to a file.
- if visible:
- if not self._isInit:
- # Deferred ROI widget init finalization
- self._finalizeInit()
-
- if not self._isConnected and plot is not None:
- plot.sigPlotSignal.connect(self._handleROIMarkerEvent)
- plot.sigActiveCurveChanged.connect(
- self._activeCurveChanged)
- self._isConnected = True
+ :param str filename: The file to which to save the ROIs
+ """
+ roilist = []
+ roidict = {}
+ for roiID, roi in self._roiDict.items():
+ roilist.append(roi.toDict())
+ roidict[roi.getName()] = roi.toDict()
+ datadict = {'ROI': {'roilist': roilist, 'roidict': roidict}}
+ dictdump.dump(datadict, filename)
- self.calculateRois()
- else:
- if self._isConnected:
- if plot is not None:
- plot.sigPlotSignal.disconnect(self._handleROIMarkerEvent)
- plot.sigActiveCurveChanged.disconnect(
- self._activeCurveChanged)
- self._isConnected = False
+ def load(self, filename):
+ """
+ Load ROI widget information from a file storing a dict of ROI.
- def _activeCurveChanged(self, *args):
- """Recompute ROIs when active curve changed."""
- self.calculateRois()
+ :param str filename: The file from which to load ROI
+ """
+ roisDict = dictdump.load(filename)
+ rois = []
- def _finalizeInit(self):
- self._isInit = True
- self.sigROIWidgetSignal.connect(self._roiSignal)
- # initialize with the ICR if no ROi existing yet
- if len(self.getRois()) is 0:
- self._roiSignal({'event': "AddROI"})
+ # Remove rawcounts and netcounts from ROIs
+ for roiDict in roisDict['ROI']['roidict'].values():
+ roiDict.pop('rawcounts', None)
+ roiDict.pop('netcounts', None)
+ rois.append(ROI._fromDict(roiDict))
+ self.setRois(rois)
-class ROITable(qt.QTableWidget):
- """Table widget displaying ROI information.
+ def showAllMarkers(self, _show=True):
+ """
- See :class:`QTableWidget` for constructor arguments.
- """
+ :param bool _show: if true show all the markers of all the ROIs
+ boundaries otherwise will only show the one of
+ the active ROI.
+ """
+ self._markersHandler.setShowAllMarkers(_show)
- sigROITableSignal = qt.Signal(object)
- """Signal of ROI table modifications.
- """
+ def setMiddleROIMarkerFlag(self, flag=True):
+ """
+ Activate or deactivate middle marker.
- def __init__(self, *args, **kwargs):
- super(ROITable, self).__init__(*args, **kwargs)
- self.setRowCount(1)
- self.labels = 'ROI', 'Type', 'From', 'To', 'Raw Counts', 'Net Counts'
- self.setColumnCount(len(self.labels))
- self.setSortingEnabled(False)
+ This allows shifting both min and max limits at once, by dragging
+ a marker located in the middle.
- for index, label in enumerate(self.labels):
- item = self.horizontalHeaderItem(index)
- if item is None:
- item = qt.QTableWidgetItem(label,
- qt.QTableWidgetItem.Type)
- item.setText(label)
- self.setHorizontalHeaderItem(index, item)
+ :param bool flag: True to activate middle ROI marker
+ """
+ self._markersHandler._middleROIMarkerFlag = flag
- self.roidict = {}
- self.roilist = []
+ def _handleROIMarkerEvent(self, ddict):
+ """Handle plot signals related to marker events."""
+ if ddict['event'] == 'markerMoved':
+ label = ddict['label']
+ roiID = self._markersHandler.getRoiID(markerID=label)
+ if roiID is not None:
+ # avoid several emission of sigROISignal
+ old = self.blockSignals(True)
+ self._markersHandler.changePosition(markerID=label,
+ x=ddict['x'])
+ self.blockSignals(old)
+ self._updateRoiInfo(roiID)
- self.building = False
- self.fillFromROIDict(roilist=self.roilist, roidict=self.roidict)
+ def showEvent(self, event):
+ self._visibilityChangedHandler(visible=True)
+ qt.QWidget.showEvent(self, event)
- self.cellClicked[(int, int)].connect(self._cellClickedSlot)
- self.cellChanged[(int, int)].connect(self._cellChangedSlot)
- verticalHeader = self.verticalHeader()
- verticalHeader.sectionClicked[int].connect(self._rowChangedSlot)
+ def hideEvent(self, event):
+ self._visibilityChangedHandler(visible=False)
+ qt.QWidget.hideEvent(self, event)
- self.__setTooltip()
+ def _visibilityChangedHandler(self, visible):
+ """Handle widget's visibility updates.
- def __setTooltip(self):
- assert(self.labels[0] == 'ROI')
- self.horizontalHeaderItem(0).setToolTip('Region of interest identifier')
- assert(self.labels[1] == 'Type')
- self.horizontalHeaderItem(1).setToolTip('Type of the ROI')
- assert(self.labels[2] == 'From')
- self.horizontalHeaderItem(2).setToolTip('X-value of the min point')
- assert(self.labels[3] == 'To')
- self.horizontalHeaderItem(3).setToolTip('X-value of the max point')
- assert(self.labels[4] == 'Raw Counts')
- self.horizontalHeaderItem(4).setToolTip('Estimation of the integral \
- between y=0 and the selected curve')
- assert(self.labels[5] == 'Net Counts')
- self.horizontalHeaderItem(5).setToolTip('Estimation of the integral \
- between the segment [maxPt, minPt] and the selected curve')
+ It is connected to plot signals only when visible.
+ """
+ if visible:
+ assert self.plot
+ if self._isConnected is False:
+ self.plot.sigPlotSignal.connect(self._handleROIMarkerEvent)
+ self.plot.sigActiveCurveChanged.connect(self._activeCurveChanged)
+ self._isConnected = True
+ self.calculateRois()
+ else:
+ if self._isConnected:
+ self.plot.sigPlotSignal.disconnect(self._handleROIMarkerEvent)
+ self.plot.sigActiveCurveChanged.disconnect(self._activeCurveChanged)
+ self._isConnected = False
+
+ def _activeCurveChanged(self, curve):
+ self.calculateRois()
+
+ def setCountsVisible(self, visible):
+ """
+ Display the columns relative to areas or not
+
+ :param bool visible: True if the columns 'Raw Area' and 'Net Area'
+ should be visible.
+ """
+ if visible is True:
+ self.showColumn(self.COLUMNS_INDEX['Raw Counts'])
+ self.showColumn(self.COLUMNS_INDEX['Net Counts'])
+ else:
+ self.hideColumn(self.COLUMNS_INDEX['Raw Counts'])
+ self.hideColumn(self.COLUMNS_INDEX['Net Counts'])
+
+ def setAreaVisible(self, visible):
+ """
+ Display the columns relative to areas or not
+
+ :param bool visible: True if the columns 'Raw Area' and 'Net Area'
+ should be visible.
+ """
+ if visible is True:
+ self.showColumn(self.COLUMNS_INDEX['Raw Area'])
+ self.showColumn(self.COLUMNS_INDEX['Net Area'])
+ else:
+ self.hideColumn(self.COLUMNS_INDEX['Raw Area'])
+ self.hideColumn(self.COLUMNS_INDEX['Net Area'])
def fillFromROIDict(self, roilist=(), roidict=None, currentroi=None):
- """Set the ROIs by providing a list of ROI names and a dictionary
- of ROI information for each ROI.
+ """
+ This function API is kept for compatibility.
+ But `setRois` should be preferred.
+ Set the ROIs by providing a list of ROI names and a dictionary
+ of ROI information for each ROI.
The ROI names must match an existing dictionary key.
The name list is used to provide an order for the ROIs.
-
The dictionary's values are sub-dictionaries containing 3
mandatory fields:
- - ``"from"``: x coordinate of the left limit, as a float
- - ``"to"``: x coordinate of the right limit, as a float
- - ``"type"``: type of ROI, as a string (e.g "channels", "energy")
+ - ``"from"``: x coordinate of the left limit, as a float
+ - ``"to"``: x coordinate of the right limit, as a float
+ - ``"type"``: type of ROI, as a string (e.g "channels", "energy")
:param roilist: List of ROI names (keys of roidict)
:type roilist: List
:param dict roidict: Dict of ROI information
:param currentroi: Name of the selected ROI or None (no selection)
"""
- if roidict is None:
- roidict = {}
-
- self.building = True
- line0 = 0
- self.roilist = []
- self.roidict = {}
- for key in roilist:
- if key in roidict.keys():
- roi = roidict[key]
- self.roilist.append(key)
- self.roidict[key] = {}
- self.roidict[key].update(roi)
- line0 = line0 + 1
- nlines = self.rowCount()
- if (line0 > nlines):
- self.setRowCount(line0)
- line = line0 - 1
- self.roidict[key]['line'] = line
- ROI = key
- roitype = "%s" % roi['type']
- fromdata = "%6g" % (roi['from'])
- todata = "%6g" % (roi['to'])
- if 'rawcounts' in roi:
- rawcounts = "%6g" % (roi['rawcounts'])
- else:
- rawcounts = " ?????? "
- if 'netcounts' in roi:
- netcounts = "%6g" % (roi['netcounts'])
- else:
- netcounts = " ?????? "
- fields = [ROI, roitype, fromdata, todata, rawcounts, netcounts]
- col = 0
- for field in fields:
- key2 = self.item(line, col)
- if key2 is None:
- key2 = qt.QTableWidgetItem(field,
- qt.QTableWidgetItem.Type)
- self.setItem(line, col, key2)
- else:
- key2.setText(field)
- if (ROI.upper() == 'ICR') or (ROI.upper() == 'DEFAULT'):
- key2.setFlags(qt.Qt.ItemIsSelectable |
- qt.Qt.ItemIsEnabled)
- else:
- if col in [0, 2, 3]:
- key2.setFlags(qt.Qt.ItemIsSelectable |
- qt.Qt.ItemIsEnabled |
- qt.Qt.ItemIsEditable)
- else:
- key2.setFlags(qt.Qt.ItemIsSelectable |
- qt.Qt.ItemIsEnabled)
- col = col + 1
- self.setRowCount(line0)
- i = 0
- for _label in self.labels:
- self.resizeColumnToContents(i)
- i = i + 1
- self.sortByColumn(2, qt.Qt.AscendingOrder)
- for i in range(len(self.roilist)):
- key = str(self.item(i, 0).text())
- self.roilist[i] = key
- self.roidict[key]['line'] = i
- if len(self.roilist) == 1:
- self.selectRow(0)
+ if roidict is not None:
+ self.setRois(roidict)
else:
- if currentroi in self.roidict.keys():
- self.selectRow(self.roidict[currentroi]['line'])
- _logger.debug("Qt4 ensureCellVisible to be implemented")
- self.building = False
+ self.setRois(roilist)
+ if currentroi:
+ self.setActiveRoi(currentroi)
- def getROIListAndDict(self):
- """Return the currently defined ROIs, as a 2-tuple
- ``(roiList, roiDict)``
- ``roiList`` is a list of ROI names.
- ``roiDict`` is a dictionary of ROI info.
+_indexNextROI = 0
- The ROI names must match an existing dictionary key.
- The name list is used to provide an order for the ROIs.
- The dictionary's values are sub-dictionaries containing 3
- fields:
+class ROI(qt.QObject):
+ """The Region Of Interest is defined by:
- - ``"from"``: x coordinate of the left limit, as a float
- - ``"to"``: x coordinate of the right limit, as a float
- - ``"type"``: type of ROI, as a string (e.g "channels", "energy")
+ - A name
+ - A type. The type is the label of the x axis. This can be used to apply or
+ not some ROI to a curve and do some post processing.
+ - The x coordinate of the left limit (fromdata)
+ - The x coordinate of the right limit (todata)
+ :param str: name of the ROI
+ :param fromdata: left limit of the roi
+ :param todata: right limit of the roi
+ :param type: type of the ROI
+ """
+
+ sigChanged = qt.Signal()
+ """Signal emitted when the ROI is edited"""
+
+ def __init__(self, name, fromdata=None, todata=None, type_=None):
+ qt.QObject.__init__(self)
+ assert type(name) is str
+ global _indexNextROI
+ self._id = _indexNextROI
+ _indexNextROI += 1
+
+ self._name = name
+ self._fromdata = fromdata
+ self._todata = todata
+ self._type = type_ or 'Default'
- :return: ordered dict as a tuple of (list of ROI names, dict of info)
+ def getID(self):
"""
- return self.roilist, self.roidict
- def _cellClickedSlot(self, *var, **kw):
- # selection changed event, get the current selection
- row = self.currentRow()
- col = self.currentColumn()
- if row >= 0 and row < len(self.roilist):
- item = self.item(row, 0)
- text = '' if item is None else str(item.text())
- self.roilist[row] = text
- self._emitSelectionChangedSignal(row, col)
+ :return int: the unique ID of the ROI
+ """
+ return self._id
- def _rowChangedSlot(self, row):
- self._emitSelectionChangedSignal(row, 0)
+ def setType(self, type_):
+ """
- def _cellChangedSlot(self, row, col):
- _logger.debug("_cellChangedSlot(%d, %d)", row, col)
- if self.building:
- return
- if col == 0:
- self.nameSlot(row, col)
+ :param str type_:
+ """
+ if self._type != type_:
+ self._type = type_
+ self.sigChanged.emit()
+
+ def getType(self):
+ """
+
+ :return str: the type of the ROI.
+ """
+ return self._type
+
+ def setName(self, name):
+ """
+ Set the name of the :class:`ROI`
+
+ :param str name:
+ """
+ if self._name != name:
+ self._name = name
+ self.sigChanged.emit()
+
+ def getName(self):
+ """
+
+ :return str: name of the :class:`ROI`
+ """
+ return self._name
+
+ def setFrom(self, frm):
+ """
+
+ :param frm: set x coordinate of the left limit
+ """
+ if self._fromdata != frm:
+ self._fromdata = frm
+ self.sigChanged.emit()
+
+ def getFrom(self):
+ """
+
+ :return: x coordinate of the left limit
+ """
+ return self._fromdata
+
+ def setTo(self, to):
+ """
+
+ :param to: x coordinate of the right limit
+ """
+ if self._todata != to:
+ self._todata = to
+ self.sigChanged.emit()
+
+ def getTo(self):
+ """
+
+ :return: x coordinate of the right limit
+ """
+ return self._todata
+
+ def getMiddle(self):
+ """
+
+ :return: middle position between 'from' and 'to' values
+ """
+ return 0.5 * (self.getFrom() + self.getTo())
+
+ def toDict(self):
+ """
+
+ :return: dict containing the roi parameters
+ """
+ ddict = {
+ 'type': self._type,
+ 'name': self._name,
+ 'from': self._fromdata,
+ 'to': self._todata,
+ }
+ if hasattr(self, '_extraInfo'):
+ ddict.update(self._extraInfo)
+ return ddict
+
+ @staticmethod
+ def _fromDict(dic):
+ assert 'name' in dic
+ roi = ROI(name=dic['name'])
+ roi._extraInfo = {}
+ for key in dic:
+ if key == 'from':
+ roi.setFrom(dic['from'])
+ elif key == 'to':
+ roi.setTo(dic['to'])
+ elif key == 'type':
+ roi.setType(dic['type'])
+ else:
+ roi._extraInfo[key] = dic[key]
+
+ return roi
+
+ def isICR(self):
+ """
+
+ :return: True if the ROI is the `ICR`
+ """
+ return self._name == 'ICR'
+
+ def computeRawAndNetCounts(self, curve):
+ """Compute the Raw and net counts in the ROI for the given curve.
+
+ - Raw count: Points values sum of the curve in the defined Region Of
+ Interest.
+
+ .. image:: img/rawCounts.png
+
+ - Net count: Raw counts minus background
+
+ .. image:: img/netCounts.png
+
+ :param CurveItem curve:
+ :return tuple: rawCount, netCount
+ """
+ assert isinstance(curve, Curve) or curve is None
+
+ if curve is None:
+ return None, None
+
+ x = curve.getXData(copy=False)
+ y = curve.getYData(copy=False)
+
+ idx = numpy.nonzero((self._fromdata <= x) &
+ (x <= self._todata))[0]
+ if len(idx):
+ xw = x[idx]
+ yw = y[idx]
+ rawCounts = yw.sum(dtype=numpy.float)
+ deltaX = xw[-1] - xw[0]
+ deltaY = yw[-1] - yw[0]
+ if deltaX > 0.0:
+ slope = (deltaY / deltaX)
+ background = yw[0] + slope * (xw - xw[0])
+ netCounts = (rawCounts -
+ background.sum(dtype=numpy.float))
+ else:
+ netCounts = 0.0
else:
- self._valueChanged(row, col)
+ rawCounts = 0.0
+ netCounts = 0.0
+ return rawCounts, netCounts
+
+ def computeRawAndNetArea(self, curve):
+ """Compute the Raw and net counts in the ROI for the given curve.
+
+ - Raw area: integral of the curve between the min ROI point and the
+ max ROI point to the y = 0 line.
- def _valueChanged(self, row, col):
- if col not in [2, 3]:
+ .. image:: img/rawArea.png
+
+ - Net area: Raw counts minus background
+
+ .. image:: img/netArea.png
+
+ :param CurveItem curve:
+ :return tuple: rawArea, netArea
+ """
+ assert isinstance(curve, Curve) or curve is None
+
+ if curve is None:
+ return None, None
+
+ x = curve.getXData(copy=False)
+ y = curve.getYData(copy=False)
+
+ y = y[(x >= self._fromdata) & (x <= self._todata)]
+ x = x[(x >= self._fromdata) & (x <= self._todata)]
+
+ if x.size is 0:
+ return 0.0, 0.0
+
+ rawArea = numpy.trapz(y, x=x)
+ # to speed up and avoid an intersection calculation we are taking the
+ # closest index to the ROI
+ closestXLeftIndex = (numpy.abs(x - self.getFrom())).argmin()
+ closestXRightIndex = (numpy.abs(x - self.getTo())).argmin()
+ yBackground = y[closestXLeftIndex], y[closestXRightIndex]
+ background = numpy.trapz(yBackground, x=x)
+ netArea = rawArea - background
+ return rawArea, netArea
+
+
+class _RoiMarkerManager(object):
+ """
+ Deal with all the ROI markers
+ """
+ def __init__(self):
+ self._roiMarkerHandlers = {}
+ self._middleROIMarkerFlag = False
+ self._showAllMarkers = False
+ self._activeRoi = None
+
+ def setActiveRoi(self, roi):
+ self._activeRoi = roi
+ self.updateAllMarkers()
+
+ def setShowAllMarkers(self, show):
+ if show != self._showAllMarkers:
+ self._showAllMarkers = show
+ self.updateAllMarkers()
+
+ def add(self, roi, markersHandler):
+ assert isinstance(roi, ROI)
+ assert isinstance(markersHandler, _RoiMarkerHandler)
+ if roi.getID() in self._roiMarkerHandlers:
+ raise ValueError('roi with the same ID already existing')
+ else:
+ self._roiMarkerHandlers[roi.getID()] = markersHandler
+
+ def getMarkerHandler(self, roiID):
+ if roiID in self._roiMarkerHandlers:
+ return self._roiMarkerHandlers[roiID]
+ else:
+ return None
+
+ def clear(self):
+ roisHandler = list(self._roiMarkerHandlers.values())
+ for roiHandler in roisHandler:
+ self.remove(roiHandler.roi)
+
+ def remove(self, roi):
+ if roi is None:
return
- item = self.item(row, col)
- if item is None:
+ assert isinstance(roi, ROI)
+ if roi.getID() in self._roiMarkerHandlers:
+ self._roiMarkerHandlers[roi.getID()].clear()
+ del self._roiMarkerHandlers[roi.getID()]
+
+ def hasMarker(self, markerID):
+ assert type(markerID) is str
+ return self.getMarker(markerID) is not None
+
+ def changePosition(self, markerID, x):
+ markerHandler = self.getMarker(markerID)
+ if markerHandler is None:
+ raise ValueError('Marker %s not register' % markerID)
+ markerHandler.changePosition(markerID=markerID, x=x)
+
+ def updateMarker(self, markerID):
+ markerHandler = self.getMarker(markerID)
+ if markerHandler is None:
+ raise ValueError('Marker %s not register' % markerID)
+ roiID = self.getRoiID(markerID)
+ visible = (self._activeRoi and self._activeRoi.getID() == roiID) or self._showAllMarkers is True
+ markerHandler.setVisible(visible)
+ markerHandler.updateAllMarkers()
+
+ def updateRoiMarkers(self, roiID):
+ if roiID in self._roiMarkerHandlers:
+ visible = ((self._activeRoi and self._activeRoi.getID() == roiID)
+ or self._showAllMarkers is True)
+ _roi = self._roiMarkerHandlers[roiID]._roi()
+ if _roi and not _roi.isICR():
+ self._roiMarkerHandlers[roiID].showMiddleMarker(self._middleROIMarkerFlag)
+ self._roiMarkerHandlers[roiID].setVisible(visible)
+ self._roiMarkerHandlers[roiID].updateMarkers()
+
+ def getMarker(self, markerID):
+ assert type(markerID) is str
+ for marker in list(self._roiMarkerHandlers.values()):
+ if marker.hasMarker(markerID):
+ return marker
+
+ def updateMarkers(self):
+ for markerHandler in list(self._roiMarkerHandlers.values()):
+ markerHandler.updateMarkers()
+
+ def getRoiID(self, markerID):
+ for roiID, markerHandler in self._roiMarkerHandlers.items():
+ if markerHandler.hasMarker(markerID):
+ return roiID
+ return None
+
+ def setShowMiddleMarkers(self, show):
+ self._middleROIMarkerFlag = show
+ self._roiMarkerHandlers.updateAllMarkers()
+
+ def updateAllMarkers(self):
+ for roiID in self._roiMarkerHandlers:
+ self.updateRoiMarkers(roiID)
+
+ def getVisibleRois(self):
+ res = {}
+ for roiID, roiHandler in self._roiMarkerHandlers.items():
+ markers = (roiHandler.getMarker('min'), roiHandler.getMarker('max'),
+ roiHandler.getMarker('middle'))
+ for marker in markers:
+ if marker.isVisible():
+ if roiID not in res:
+ res[roiID] = []
+ res[roiID].append(marker)
+ return res
+
+
+class _RoiMarkerHandler(object):
+ """Used to deal with ROI markers used in ROITable"""
+ def __init__(self, roi, plot):
+ assert roi and isinstance(roi, ROI)
+ assert plot
+
+ self._roi = weakref.ref(roi)
+ self._plot = weakref.ref(plot)
+ self._draggable = False if roi.isICR() else True
+ self._color = 'black' if roi.isICR() else 'blue'
+ self._displayMidMarker = False
+ self._visible = True
+
+ @property
+ def draggable(self):
+ return self._draggable
+
+ @property
+ def plot(self):
+ return self._plot()
+
+ def clear(self):
+ if self.plot and self.roi:
+ self.plot.removeMarker(self._markerID('min'))
+ self.plot.removeMarker(self._markerID('max'))
+ self.plot.removeMarker(self._markerID('middle'))
+
+ @property
+ def roi(self):
+ return self._roi()
+
+ def setVisible(self, visible):
+ if visible != self._visible:
+ self._visible = visible
+ self.updateMarkers()
+
+ def showMiddleMarker(self, visible):
+ if self.draggable is False and visible is True:
+ _logger.warning("ROI is not draggable. Won't display middle marker")
return
- text = str(item.text())
- try:
- value = float(text)
- except:
+ self._displayMidMarker = visible
+ self.getMarker('middle').setVisible(self._displayMidMarker)
+
+ def updateMarkers(self):
+ if self.roi is None:
return
- if row >= len(self.roilist):
- _logger.debug("deleting???")
+ self._updateMinMarkerPos()
+ self._updateMaxMarkerPos()
+ self._updateMiddleMarkerPos()
+
+ def _updateMinMarkerPos(self):
+ self.getMarker('min').setPosition(x=self.roi.getFrom(), y=None)
+ self.getMarker('min').setVisible(self._visible)
+
+ def _updateMaxMarkerPos(self):
+ self.getMarker('max').setPosition(x=self.roi.getTo(), y=None)
+ self.getMarker('max').setVisible(self._visible)
+
+ def _updateMiddleMarkerPos(self):
+ self.getMarker('middle').setPosition(x=self.roi.getMiddle(), y=None)
+ self.getMarker('middle').setVisible(self._displayMidMarker and self._visible)
+
+ def getMarker(self, markerType):
+ if self.plot is None:
+ return None
+ assert markerType in ('min', 'max', 'middle')
+ if self.plot._getMarker(self._markerID(markerType)) is None:
+ assert self.roi
+ if markerType == 'min':
+ val = self.roi.getFrom()
+ elif markerType == 'max':
+ val = self.roi.getTo()
+ else:
+ val = self.roi.getMiddle()
+
+ _color = self._color
+ if markerType == 'middle':
+ _color = 'yellow'
+ self.plot.addXMarker(val,
+ legend=self._markerID(markerType),
+ text=self.getMarkerName(markerType),
+ color=_color,
+ draggable=self.draggable)
+ return self.plot._getMarker(self._markerID(markerType))
+
+ def _markerID(self, markerType):
+ assert markerType in ('min', 'max', 'middle')
+ assert self.roi
+ return '_'.join((str(self.roi.getID()), markerType))
+
+ def getMarkerName(self, markerType):
+ assert markerType in ('min', 'max', 'middle')
+ assert self.roi
+ return ' '.join((self.roi.getName(), markerType))
+
+ def updateTexts(self):
+ self.getMarker('min').setText(self.getMarkerName('min'))
+ self.getMarker('max').setText(self.getMarkerName('max'))
+ self.getMarker('middle').setText(self.getMarkerName('middle'))
+
+ def changePosition(self, markerID, x):
+ assert self.hasMarker(markerID)
+ markerType = self._getMarkerType(markerID)
+ assert markerType is not None
+ if self.roi is None:
return
- item = self.item(row, 0)
- if item is None:
- text = ""
+ if markerType == 'min':
+ self.roi.setFrom(x)
+ self._updateMiddleMarkerPos()
+ elif markerType == 'max':
+ self.roi.setTo(x)
+ self._updateMiddleMarkerPos()
else:
- text = str(item.text())
- if not len(text):
- return
- if col == 2:
- self.roidict[text]['from'] = value
- elif col == 3:
- self.roidict[text]['to'] = value
- self._emitSelectionChangedSignal(row, col)
-
- def nameSlot(self, row, col):
- if col != 0:
- return
- if row >= len(self.roilist):
- _logger.debug("deleting???")
- return
- item = self.item(row, col)
- if item is None:
- text = ""
+ delta = x - 0.5 * (self.roi.getFrom() + self.roi.getTo())
+ self.roi.setFrom(self.roi.getFrom() + delta)
+ self.roi.setTo(self.roi.getTo() + delta)
+ self._updateMinMarkerPos()
+ self._updateMaxMarkerPos()
+
+ def hasMarker(self, marker):
+ return marker in (self._markerID('min'),
+ self._markerID('max'),
+ self._markerID('middle'))
+
+ def _getMarkerType(self, markerID):
+ if markerID.endswith('_min'):
+ return 'min'
+ elif markerID.endswith('_max'):
+ return 'max'
+ elif markerID.endswith('_middle'):
+ return 'middle'
else:
- text = str(item.text())
- if len(text) and (text not in self.roilist):
- old = self.roilist[row]
- self.roilist[row] = text
- self.roidict[text] = {}
- self.roidict[text].update(self.roidict[old])
- del self.roidict[old]
- self._emitSelectionChangedSignal(row, col)
-
- def _emitSelectionChangedSignal(self, row, col):
- ddict = {}
- ddict['event'] = "selectionChanged"
- ddict['row'] = row
- ddict['col'] = col
- ddict['roi'] = self.roidict[self.roilist[row]]
- ddict['key'] = self.roilist[row]
- ddict['colheader'] = self.labels[col]
- ddict['rowheader'] = "%d" % row
- self.sigROITableSignal.emit(ddict)
+ return None
class CurvesROIDockWidget(qt.QDockWidget):
@@ -1007,6 +1540,8 @@ class CurvesROIDockWidget(qt.QDockWidget):
def __init__(self, parent=None, plot=None, name=None):
super(CurvesROIDockWidget, self).__init__(name, parent)
+ assert plot is not None
+ self.plot = plot
self.roiWidget = CurvesROIWidget(self, name, plot=plot)
"""Main widget of type :class:`CurvesROIWidget`"""
@@ -1016,12 +1551,15 @@ class CurvesROIDockWidget(qt.QDockWidget):
self.calculateROIs = self.calculateRois = self.roiWidget.calculateRois
self.setRois = self.roiWidget.setRois
self.getRois = self.roiWidget.getRois
+
self.roiWidget.sigROISignal.connect(self._forwardSigROISignal)
- self.currentROI = self.roiWidget.currentROI
self.layout().setContentsMargins(0, 0, 0, 0)
self.setWidget(self.roiWidget)
+ self.setAreaVisible = self.roiWidget.roiTable.setAreaVisible
+ self.setCountsVisible = self.roiWidget.roiTable.setCountsVisible
+
def _forwardSigROISignal(self, ddict):
# emit deprecated signal for backward compatibility (silx < 0.7)
self.sigROISignal.emit(ddict)
@@ -1042,3 +1580,7 @@ class CurvesROIDockWidget(qt.QDockWidget):
"""
self.raise_()
qt.QDockWidget.showEvent(self, event)
+
+ @property
+ def currentROI(self):
+ return self.roiWidget.currentRoi
diff --git a/silx/gui/plot/MaskToolsWidget.py b/silx/gui/plot/MaskToolsWidget.py
index 990e479..9d727e7 100644
--- a/silx/gui/plot/MaskToolsWidget.py
+++ b/silx/gui/plot/MaskToolsWidget.py
@@ -35,7 +35,7 @@ from __future__ import division
__authors__ = ["T. Vincent", "P. Knobel"]
__license__ = "MIT"
-__date__ = "29/08/2018"
+__date__ = "15/02/2019"
import os
@@ -57,10 +57,7 @@ from .. import qt
from silx.third_party.EdfFile import EdfFile
from silx.third_party.TiffIO import TiffIO
-try:
- import fabio
-except ImportError:
- fabio = None
+import fabio
_logger = logging.getLogger(__name__)
@@ -135,8 +132,6 @@ class ImageMask(BaseMask):
self._saveToHdf5(filename, self.getMask(copy=False))
elif kind == 'msk':
- if fabio is None:
- raise ImportError("Fit2d mask files can't be written: Fabio module is not available")
try:
data = self.getMask(copy=False)
image = fabio.fabioimage.FabioImage(data=data)
@@ -250,6 +245,19 @@ class ImageMask(BaseMask):
rows, cols = shapes.circle_fill(crow, ccol, radius)
self.updatePoints(level, rows, cols, mask)
+ def updateEllipse(self, level, crow, ccol, radius_r, radius_c, mask=True):
+ """Mask/Unmask an ellipse of the given mask level.
+
+ :param int level: Mask level to update.
+ :param int crow: Row of the center of the ellipse
+ :param int ccol: Column of the center of the ellipse
+ :param float radius_r: Radius of the ellipse in the row
+ :param float radius_c: Radius of the ellipse in the column
+ :param bool mask: True to mask (default), False to unmask.
+ """
+ rows, cols = shapes.ellipse_fill(crow, ccol, radius_r, radius_c)
+ self.updatePoints(level, rows, cols, mask)
+
def updateLine(self, level, row0, col0, row1, col1, width, mask=True):
"""Mask/Unmask a line of the given mask level.
@@ -300,6 +308,10 @@ class MaskToolsWidget(BaseMaskToolsWidget):
_logger.error('Not an image, shape: %d', len(mask.shape))
return None
+ # Handle mask with single level
+ if self.multipleMasks() == 'single':
+ mask = numpy.array(mask != 0, dtype=numpy.uint8)
+
# if mask has not changed, do nothing
if numpy.array_equal(mask, self.getSelectionMask()):
return mask.shape
@@ -501,8 +513,6 @@ class MaskToolsWidget(BaseMaskToolsWidget):
_logger.debug("Backtrace", exc_info=True)
raise e
elif extension == "msk":
- if fabio is None:
- raise ImportError("Fit2d mask files can't be read: Fabio module is not available")
try:
mask = fabio.open(filename).data
except Exception as e:
@@ -682,41 +692,51 @@ class MaskToolsWidget(BaseMaskToolsWidget):
level = self.levelSpinBox.value()
- if (self._drawingMode == 'rectangle' and
- event['event'] == 'drawingFinished'):
- # Convert from plot to array coords
- doMask = self._isMasking()
- ox, oy = self._origin
- sx, sy = self._scale
-
- height = int(abs(event['height'] / sy))
- width = int(abs(event['width'] / sx))
-
- row = int((event['y'] - oy) / sy)
- if sy < 0:
- row -= height
-
- col = int((event['x'] - ox) / sx)
- if sx < 0:
- col -= width
-
- self._mask.updateRectangle(
- level,
- row=row,
- col=col,
- height=height,
- width=width,
- mask=doMask)
- self._mask.commit()
+ if self._drawingMode == 'rectangle':
+ if event['event'] == 'drawingFinished':
+ # Convert from plot to array coords
+ doMask = self._isMasking()
+ ox, oy = self._origin
+ sx, sy = self._scale
+
+ height = int(abs(event['height'] / sy))
+ width = int(abs(event['width'] / sx))
+
+ row = int((event['y'] - oy) / sy)
+ if sy < 0:
+ row -= height
+
+ col = int((event['x'] - ox) / sx)
+ if sx < 0:
+ col -= width
+
+ self._mask.updateRectangle(
+ level,
+ row=row,
+ col=col,
+ height=height,
+ width=width,
+ mask=doMask)
+ self._mask.commit()
- elif (self._drawingMode == 'polygon' and
- event['event'] == 'drawingFinished'):
- doMask = self._isMasking()
- # Convert from plot to array coords
- vertices = (event['points'] - self._origin) / self._scale
- vertices = vertices.astype(numpy.int)[:, (1, 0)] # (row, col)
- self._mask.updatePolygon(level, vertices, doMask)
- self._mask.commit()
+ elif self._drawingMode == 'ellipse':
+ if event['event'] == 'drawingFinished':
+ doMask = self._isMasking()
+ # Convert from plot to array coords
+ center = (event['points'][0] - self._origin) / self._scale
+ size = event['points'][1] / self._scale
+ center = center.astype(numpy.int) # (row, col)
+ self._mask.updateEllipse(level, center[1], center[0], size[1], size[0], doMask)
+ self._mask.commit()
+
+ elif self._drawingMode == 'polygon':
+ if event['event'] == 'drawingFinished':
+ doMask = self._isMasking()
+ # Convert from plot to array coords
+ vertices = (event['points'] - self._origin) / self._scale
+ vertices = vertices.astype(numpy.int)[:, (1, 0)] # (row, col)
+ self._mask.updatePolygon(level, vertices, doMask)
+ self._mask.commit()
elif self._drawingMode == 'pencil':
doMask = self._isMasking()
@@ -743,6 +763,8 @@ class MaskToolsWidget(BaseMaskToolsWidget):
self._lastPencilPos = None
else:
self._lastPencilPos = row, col
+ else:
+ _logger.error("Drawing mode %s unsupported", self._drawingMode)
def _loadRangeFromColormapTriggered(self):
"""Set range from active image colormap range"""
diff --git a/silx/gui/plot/PlotInteraction.py b/silx/gui/plot/PlotInteraction.py
index 356bda6..27abd10 100644
--- a/silx/gui/plot/PlotInteraction.py
+++ b/silx/gui/plot/PlotInteraction.py
@@ -26,7 +26,7 @@
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "24/04/2018"
+__date__ = "15/02/2019"
import math
@@ -96,10 +96,18 @@ class _PlotInteraction(object):
fill = fill != 'none' # TODO not very nice either
+ greyed = colors.greyed(color)[0]
+ if greyed < 0.5:
+ color2 = "white"
+ else:
+ color2 = "black"
+
self.plot.addItem(points[:, 0], points[:, 1], legend=legend,
replace=False,
- shape=shape, color=color, fill=fill,
+ shape=shape, fill=fill,
+ color=color, linebgcolor=color2, linestyle="--",
overlay=True)
+
self._selectionAreas.add(legend)
def resetSelectionArea(self):
@@ -274,6 +282,8 @@ class Zoom(_ZoomOnWheel):
and zoom on mouse wheel.
"""
+ SURFACE_THRESHOLD = 5
+
def __init__(self, plot, color):
self.color = color
@@ -347,35 +357,44 @@ class Zoom(_ZoomOnWheel):
self.setSelectionArea(corners, fill='none', color=self.color)
- def endDrag(self, startPos, endPos):
- x0, y0 = startPos
- x1, y1 = endPos
+ def _zoom(self, x0, y0, x1, y1):
+ """Zoom to the rectangle view x0,y0 x1,y1.
+ """
+ startPos = x0, y0
+ endPos = x1, y1
+
+ # Store current zoom state in stack
+ self.plot.getLimitsHistory().push()
- if x0 != x1 or y0 != y1: # Avoid empty zoom area
- # Store current zoom state in stack
- self.plot.getLimitsHistory().push()
+ if self.plot.isKeepDataAspectRatio():
+ x0, y0, x1, y1 = self._areaWithAspectRatio(x0, y0, x1, y1)
+
+ # Convert to data space and set limits
+ x0, y0 = self.plot.pixelToData(x0, y0, check=False)
- if self.plot.isKeepDataAspectRatio():
- x0, y0, x1, y1 = self._areaWithAspectRatio(x0, y0, x1, y1)
+ dataPos = self.plot.pixelToData(
+ startPos[0], startPos[1], axis="right", check=False)
+ y2_0 = dataPos[1]
- # Convert to data space and set limits
- x0, y0 = self.plot.pixelToData(x0, y0, check=False)
+ x1, y1 = self.plot.pixelToData(x1, y1, check=False)
- dataPos = self.plot.pixelToData(
- startPos[0], startPos[1], axis="right", check=False)
- y2_0 = dataPos[1]
+ dataPos = self.plot.pixelToData(
+ endPos[0], endPos[1], axis="right", check=False)
+ y2_1 = dataPos[1]
- x1, y1 = self.plot.pixelToData(x1, y1, check=False)
+ xMin, xMax = min(x0, x1), max(x0, x1)
+ yMin, yMax = min(y0, y1), max(y0, y1)
+ y2Min, y2Max = min(y2_0, y2_1), max(y2_0, y2_1)
- dataPos = self.plot.pixelToData(
- endPos[0], endPos[1], axis="right", check=False)
- y2_1 = dataPos[1]
+ self.plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
- xMin, xMax = min(x0, x1), max(x0, x1)
- yMin, yMax = min(y0, y1), max(y0, y1)
- y2Min, y2Max = min(y2_0, y2_1), max(y2_0, y2_1)
+ def endDrag(self, startPos, endPos):
+ x0, y0 = startPos
+ x1, y1 = endPos
- self.plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
+ if abs(x0 - x1) * abs(y0 - y1) >= self.SURFACE_THRESHOLD:
+ # Avoid empty zoom area
+ self._zoom(x0, y0, x1, y1)
self.resetSelectionArea()
@@ -544,7 +563,6 @@ class SelectPolygon(Select):
return self.DRAG_THRESHOLD_DIST * ratio
-
class Select2Points(Select):
"""Base class for drawing selection based on 2 input points."""
class Idle(State):
@@ -603,6 +621,87 @@ class Select2Points(Select):
self.cancelSelect()
+class SelectEllipse(Select2Points):
+ """Drawing ellipse selection area state machine."""
+ def beginSelect(self, x, y):
+ self.center = self.plot.pixelToData(x, y)
+ assert self.center is not None
+
+ def _getEllipseSize(self, pointInEllipse):
+ """
+ Returns the size from the center to the bounding box of the ellipse.
+
+ :param Tuple[float,float] pointInEllipse: A point of the ellipse
+ :rtype: Tuple[float,float]
+ """
+ x = abs(self.center[0] - pointInEllipse[0])
+ y = abs(self.center[1] - pointInEllipse[1])
+ if x == 0 or y == 0:
+ return x, y
+ # Ellipse definitions
+ # e: eccentricity
+ # a: length fron center to bounding box width
+ # b: length fron center to bounding box height
+ # Equations
+ # (1) b < a
+ # (2) For x,y a point in the ellipse: x^2/a^2 + y^2/b^2 = 1
+ # (3) b = a * sqrt(1-e^2)
+ # (4) e = sqrt(a^2 - b^2) / a
+
+ # The eccentricity of the ellipse defined by a,b=x,y is the same
+ # as the one we are searching for.
+ swap = x < y
+ if swap:
+ x, y = y, x
+ e = math.sqrt(x**2 - y**2) / x
+ # From (2) using (3) to replace b
+ # a^2 = x^2 + y^2 / (1-e^2)
+ a = math.sqrt(x**2 + y**2 / (1.0 - e**2))
+ b = a * math.sqrt(1 - e**2)
+ if swap:
+ a, b = b, a
+ return a, b
+
+ def select(self, x, y):
+ dataPos = self.plot.pixelToData(x, y)
+ assert dataPos is not None
+ width, height = self._getEllipseSize(dataPos)
+
+ # Circle used for circle preview
+ nbpoints = 27.
+ angles = numpy.arange(nbpoints) * numpy.pi * 2.0 / nbpoints
+ circleShape = numpy.array((numpy.cos(angles) * width,
+ numpy.sin(angles) * height)).T
+ circleShape += numpy.array(self.center)
+
+ self.setSelectionArea(circleShape,
+ shape="polygon",
+ fill='hatch',
+ color=self.color)
+
+ eventDict = prepareDrawingSignal('drawingProgress',
+ 'ellipse',
+ (self.center, (width, height)),
+ self.parameters)
+ self.plot.notify(**eventDict)
+
+ def endSelect(self, x, y):
+ self.resetSelectionArea()
+
+ dataPos = self.plot.pixelToData(x, y)
+ assert dataPos is not None
+ width, height = self._getEllipseSize(dataPos)
+
+ eventDict = prepareDrawingSignal('drawingFinished',
+ 'ellipse',
+ (self.center, (width, height)),
+ self.parameters)
+ self.plot.notify(**eventDict)
+
+ def cancelSelect(self):
+ self.resetSelectionArea()
+
+
class SelectRectangle(Select2Points):
"""Drawing rectangle selection area state machine."""
def beginSelect(self, x, y):
@@ -1488,6 +1587,7 @@ class PlotInteraction(object):
_DRAW_MODES = {
'polygon': SelectPolygon,
'rectangle': SelectRectangle,
+ 'ellipse': SelectEllipse,
'line': SelectLine,
'vline': SelectVLine,
'hline': SelectHLine,
diff --git a/silx/gui/plot/PlotToolButtons.py b/silx/gui/plot/PlotToolButtons.py
index f6291b5..cd1a43f 100644
--- a/silx/gui/plot/PlotToolButtons.py
+++ b/silx/gui/plot/PlotToolButtons.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -45,8 +45,9 @@ import weakref
from .. import icons
from .. import qt
+from ... import config
-from .items import SymbolMixIn
+from .items import SymbolMixIn, Scatter
_logger = logging.getLogger(__name__)
@@ -250,24 +251,24 @@ class ProfileOptionToolButton(PlotToolButton):
self.STATE = {}
# is down
self.STATE['sum', "icon"] = icons.getQIcon('math-sigma')
- self.STATE['sum', "state"] = "compute profile sum"
- self.STATE['sum', "action"] = "compute profile sum"
+ self.STATE['sum', "state"] = "Compute profile sum"
+ self.STATE['sum', "action"] = "Compute profile sum"
# keep ration
self.STATE['mean', "icon"] = icons.getQIcon('math-mean')
- self.STATE['mean', "state"] = "compute profile mean"
- self.STATE['mean', "action"] = "compute profile mean"
+ self.STATE['mean', "state"] = "Compute profile mean"
+ self.STATE['mean', "action"] = "Compute profile mean"
- sumAction = self._createAction('sum')
- sumAction.triggered.connect(self.setSum)
- sumAction.setIconVisibleInMenu(True)
+ self.sumAction = self._createAction('sum')
+ self.sumAction.triggered.connect(self.setSum)
+ self.sumAction.setIconVisibleInMenu(True)
- meanAction = self._createAction('mean')
- meanAction.triggered.connect(self.setMean)
- meanAction.setIconVisibleInMenu(True)
+ self.meanAction = self._createAction('mean')
+ self.meanAction.triggered.connect(self.setMean)
+ self.meanAction.setIconVisibleInMenu(True)
menu = qt.QMenu(self)
- menu.addAction(sumAction)
- menu.addAction(meanAction)
+ menu.addAction(self.sumAction)
+ menu.addAction(self.meanAction)
self.setMenu(menu)
self.setPopupMode(qt.QToolButton.InstantPopup)
self.setMean()
@@ -351,36 +352,36 @@ class ProfileToolButton(PlotToolButton):
self._profileDimensionChanged(2)
-class SymbolToolButton(PlotToolButton):
- """A tool button with a drop-down menu to control symbol size and marker.
+
+class _SymbolToolButtonBase(PlotToolButton):
+ """Base class for PlotToolButton setting marker and size.
:param parent: See QWidget
:param plot: The `~silx.gui.plot.PlotWidget` to control
"""
def __init__(self, parent=None, plot=None):
- super(SymbolToolButton, self).__init__(parent=parent, plot=plot)
+ super(_SymbolToolButtonBase, self).__init__(parent=parent, plot=plot)
- self.setToolTip('Set symbol size and marker')
- self.setIcon(icons.getQIcon('plot-symbols'))
-
- menu = qt.QMenu(self)
-
- # Size slider
+ def _addSizeSliderToMenu(self, menu):
+ """Add a slider to set size to the given menu
+ :param QMenu menu:
+ """
slider = qt.QSlider(qt.Qt.Horizontal)
slider.setRange(1, 20)
- slider.setValue(SymbolMixIn._DEFAULT_SYMBOL_SIZE)
+ slider.setValue(config.DEFAULT_PLOT_SYMBOL_SIZE)
slider.setTracking(False)
slider.valueChanged.connect(self._sizeChanged)
widgetAction = qt.QWidgetAction(menu)
widgetAction.setDefaultWidget(slider)
menu.addAction(widgetAction)
- menu.addSeparator()
-
- # Marker actions
+ def _addSymbolsToMenu(self, menu):
+ """Add symbols to the given menu
+ :param QMenu menu:
+ """
for marker, name in zip(SymbolMixIn.getSupportedSymbols(),
SymbolMixIn.getSupportedSymbolNames()):
action = qt.QAction(name, menu)
@@ -389,9 +390,6 @@ class SymbolToolButton(PlotToolButton):
functools.partial(self._markerChanged, marker))
menu.addAction(action)
- self.setMenu(menu)
- self.setPopupMode(qt.QToolButton.InstantPopup)
-
def _sizeChanged(self, value):
"""Manage slider value changed
@@ -417,3 +415,78 @@ class SymbolToolButton(PlotToolButton):
for item in plot._getItems(withhidden=True):
if isinstance(item, SymbolMixIn):
item.setSymbol(marker)
+
+
+class SymbolToolButton(_SymbolToolButtonBase):
+ """A tool button with a drop-down menu to control symbol size and marker.
+
+ :param parent: See QWidget
+ :param plot: The `~silx.gui.plot.PlotWidget` to control
+ """
+
+ def __init__(self, parent=None, plot=None):
+ super(SymbolToolButton, self).__init__(parent=parent, plot=plot)
+
+ self.setToolTip('Set symbol size and marker')
+ self.setIcon(icons.getQIcon('plot-symbols'))
+
+ menu = qt.QMenu(self)
+ self._addSizeSliderToMenu(menu)
+ menu.addSeparator()
+ self._addSymbolsToMenu(menu)
+
+ self.setMenu(menu)
+ self.setPopupMode(qt.QToolButton.InstantPopup)
+
+
+class ScatterVisualizationToolButton(_SymbolToolButtonBase):
+ """QToolButton to select the visualization mode of scatter plot
+
+ :param parent: See QWidget
+ :param plot: The `~silx.gui.plot.PlotWidget` to control
+ """
+
+ def __init__(self, parent=None, plot=None):
+ super(ScatterVisualizationToolButton, self).__init__(
+ parent=parent, plot=plot)
+
+ self.setToolTip(
+ 'Set scatter visualization mode, symbol marker and size')
+ self.setIcon(icons.getQIcon('eye'))
+
+ menu = qt.QMenu(self)
+
+ # Add visualization modes
+
+ for mode in Scatter.supportedVisualizations():
+ name = mode.value.capitalize()
+ action = qt.QAction(name, menu)
+ action.setCheckable(False)
+ action.triggered.connect(
+ functools.partial(self._visualizationChanged, mode))
+ menu.addAction(action)
+
+ menu.addSeparator()
+
+ submenu = menu.addMenu(icons.getQIcon('plot-symbols'), "Symbol")
+ self._addSymbolsToMenu(submenu)
+
+ submenu = menu.addMenu(icons.getQIcon('plot-symbols'), "Symbol Size")
+ self._addSizeSliderToMenu(submenu)
+
+ self.setMenu(menu)
+ self.setPopupMode(qt.QToolButton.InstantPopup)
+
+ def _visualizationChanged(self, mode):
+ """Handle change of visualization mode.
+
+ :param ScatterVisualizationMixIn.Visualization mode:
+ The visualization mode to use for scatter
+ """
+ plot = self.plot()
+ if plot is None:
+ return
+
+ for item in plot._getItems(withhidden=True):
+ if isinstance(item, Scatter):
+ item.setVisualization(mode)
diff --git a/silx/gui/plot/PlotWidget.py b/silx/gui/plot/PlotWidget.py
index e023a21..9b9b4d2 100644
--- a/silx/gui/plot/PlotWidget.py
+++ b/silx/gui/plot/PlotWidget.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -31,24 +31,34 @@ from __future__ import division
__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
-__date__ = "12/10/2018"
+__date__ = "21/12/2018"
+
+import logging
+
+_logger = logging.getLogger(__name__)
from collections import OrderedDict, namedtuple
+try:
+ from collections import abc
+except ImportError: # Python2 support
+ import collections as abc
from contextlib import contextmanager
import datetime as dt
import itertools
-import logging
+import warnings
import numpy
import silx
from silx.utils.weakref import WeakMethodProxy
-from silx.utils import deprecation
from silx.utils.property import classproperty
from silx.utils.deprecation import deprecated
-# Import matplotlib backend here to init matplotlib our way
-from .backends.BackendMatplotlib import BackendMatplotlibQt
+try:
+ # Import matplotlib now to init matplotlib our way
+ from . import matplotlib
+except ImportError:
+ _logger.debug("matplotlib not available")
from ..colors import Colormap
from .. import colors
@@ -65,7 +75,6 @@ from .. import qt
from ._utils.panzoom import ViewConstraints
from ...gui.plot._utils.dtime_ticklayout import timestamp
-_logger = logging.getLogger(__name__)
_COLORDICT = colors.COLORDICT
@@ -99,7 +108,7 @@ class PlotWidget(qt.QMainWindow):
# TODO: Can be removed for silx 0.10
@classproperty
- @deprecation.deprecated(replacement="silx.config.DEFAULT_PLOT_BACKEND", since_version="0.8", skip_backtrace_count=2)
+ @deprecated(replacement="silx.config.DEFAULT_PLOT_BACKEND", since_version="0.8", skip_backtrace_count=2)
def DEFAULT_BACKEND(self):
"""Class attribute setting the default backend for all instances."""
return silx.config.DEFAULT_PLOT_BACKEND
@@ -193,21 +202,12 @@ class PlotWidget(qt.QMainWindow):
It provides the visible state.
"""
- def __init__(self, parent=None, backend=None,
- legends=False, callback=None, **kw):
+ def __init__(self, parent=None, backend=None):
self._autoreplot = False
self._dirty = False
self._cursorInPlot = False
self.__muteActiveItemChanged = False
- if kw:
- _logger.warning(
- 'deprecated: __init__ extra arguments: %s', str(kw))
- if legends:
- _logger.warning('deprecated: __init__ legend argument')
- if callback:
- _logger.warning('deprecated: __init__ callback argument')
-
self._panWithArrowKeys = True
self._viewConstrains = None
@@ -218,27 +218,8 @@ class PlotWidget(qt.QMainWindow):
else:
self.setWindowTitle('PlotWidget')
- if backend is None:
- backend = silx.config.DEFAULT_PLOT_BACKEND
-
- if hasattr(backend, "__call__"):
- self._backend = backend(self, parent)
-
- elif hasattr(backend, "lower"):
- lowerCaseString = backend.lower()
- if lowerCaseString in ("matplotlib", "mpl"):
- backendClass = BackendMatplotlibQt
- elif lowerCaseString in ('gl', 'opengl'):
- from .backends.BackendOpenGL import BackendOpenGL
- backendClass = BackendOpenGL
- elif lowerCaseString == 'none':
- from .backends.BackendBase import BackendBase as backendClass
- else:
- raise ValueError("Backend not supported %s" % backend)
- self._backend = backendClass(self, parent)
-
- else:
- raise ValueError("Backend not supported %s" % str(backend))
+ self._backend = None
+ self._setBackend(backend)
self.setCallback() # set _callback
@@ -258,6 +239,12 @@ class PlotWidget(qt.QMainWindow):
self._activeLegend = {'curve': None, 'image': None,
'scatter': None}
+ # plot colors (updated later to sync backend)
+ self._foregroundColor = 0., 0., 0., 1.
+ self._gridColor = .7, .7, .7, 1.
+ self._backgroundColor = 1., 1., 1., 1.
+ self._dataBackgroundColor = None
+
# default properties
self._cursorConfiguration = None
@@ -275,7 +262,7 @@ class PlotWidget(qt.QMainWindow):
self.setDefaultColormap() # Init default colormap
- self.setDefaultPlotPoints(False)
+ self.setDefaultPlotPoints(silx.config.DEFAULT_PLOT_CURVE_SYMBOL_MODE)
self.setDefaultPlotLines(True)
self._limitsHistory = LimitsHistory(self)
@@ -306,9 +293,76 @@ class PlotWidget(qt.QMainWindow):
self.setGraphYLimits(0., 100., axis='right')
self.setGraphYLimits(0., 100., axis='left')
+ # Sync backend colors with default ones
+ self._foregroundColorsUpdated()
+ self._backgroundColorsUpdated()
+
+ def __getBackendClass(self, backend):
+ """Returns backend class corresponding to backend.
+
+ If multiple backends are provided, the first available one is used.
+
+ :param Union[str,BackendBase,Iterable] backend:
+ The name of the backend or its class or an iterable of those.
+ :rtype: BackendBase
+ :raise ValueError: In case the backend is not supported
+ :raise RuntimeError: If a backend is not available
+ """
+ if callable(backend):
+ return backend
+
+ elif isinstance(backend, str):
+ backend = backend.lower()
+ if backend in ('matplotlib', 'mpl'):
+ try:
+ from .backends.BackendMatplotlib import \
+ BackendMatplotlibQt as backendClass
+ except ImportError:
+ _logger.debug("Backtrace", exc_info=True)
+ raise ImportError("matplotlib backend is not available")
+
+ elif backend in ('gl', 'opengl'):
+ try:
+ from .backends.BackendOpenGL import \
+ BackendOpenGL as backendClass
+ except ImportError:
+ _logger.debug("Backtrace", exc_info=True)
+ raise ImportError("OpenGL backend is not available")
+
+ elif backend == 'none':
+ from .backends.BackendBase import BackendBase as backendClass
+
+ else:
+ raise ValueError("Backend not supported %s" % backend)
+
+ return backendClass
+
+ elif isinstance(backend, abc.Iterable):
+ for b in backend:
+ try:
+ return self.__getBackendClass(b)
+ except ImportError:
+ pass
+ else: # No backend was found
+ raise ValueError("No supported backend was found")
+
+ raise ValueError("Backend not supported %s" % str(backend))
+
+ def _setBackend(self, backend):
+ """Setup a new backend
+
+ :param backend: Either a str defining the backend to use
+ """
+ assert(self._backend is None)
+
+ if backend is None:
+ backend = silx.config.DEFAULT_PLOT_BACKEND
+
+ self._backend = self.__getBackendClass(backend)(self, self)
+
# TODO: Can be removed for silx 0.10
@staticmethod
- @deprecation.deprecated(replacement="silx.config.DEFAULT_PLOT_BACKEND", since_version="0.8", skip_backtrace_count=2)
+ @deprecated(replacement="silx.config.DEFAULT_PLOT_BACKEND", since_version="0.8", skip_backtrace_count=2)
def setDefaultBackend(backend):
"""Set system wide default plot backend.
@@ -349,6 +403,119 @@ class PlotWidget(qt.QMainWindow):
if self._autoreplot and not wasDirty and self.isVisible():
self._backend.postRedisplay()
+ def _foregroundColorsUpdated(self):
+ """Handle change of foreground/grid color"""
+ if self._gridColor is None:
+ gridColor = self._foregroundColor
+ else:
+ gridColor = self._gridColor
+ self._backend.setForegroundColors(
+ self._foregroundColor, gridColor)
+ self._setDirtyPlot()
+
+ def getForegroundColor(self):
+ """Returns the RGBA colors used to display the foreground of this widget
+
+ :rtype: qt.QColor
+ """
+ return qt.QColor.fromRgbF(*self._foregroundColor)
+
+ def setForegroundColor(self, color):
+ """Set the foreground color of this widget.
+
+ :param Union[List[int],List[float],QColor] color:
+ The new RGB(A) color.
+ """
+ color = colors.rgba(color)
+ if self._foregroundColor != color:
+ self._foregroundColor = color
+ self._foregroundColorsUpdated()
+
+ def getGridColor(self):
+ """Returns the RGBA colors used to display the grid lines
+
+ An invalid QColor is returned if there is no grid color,
+ in which case the foreground color is used.
+
+ :rtype: qt.QColor
+ """
+ if self._gridColor is None:
+ return qt.QColor() # An invalid color
+ else:
+ return qt.QColor.fromRgbF(*self._gridColor)
+
+ def setGridColor(self, color):
+ """Set the grid lines color
+
+ :param Union[List[int],List[float],QColor,None] color:
+ The new RGB(A) color.
+ """
+ if isinstance(color, qt.QColor) and not color.isValid():
+ color = None
+ if color is not None:
+ color = colors.rgba(color)
+ if self._gridColor != color:
+ self._gridColor = color
+ self._foregroundColorsUpdated()
+
+ def _backgroundColorsUpdated(self):
+ """Handle change of background/data background color"""
+ if self._dataBackgroundColor is None:
+ dataBGColor = self._backgroundColor
+ else:
+ dataBGColor = self._dataBackgroundColor
+ self._backend.setBackgroundColors(
+ self._backgroundColor, dataBGColor)
+ self._setDirtyPlot()
+
+ def getBackgroundColor(self):
+ """Returns the RGBA colors used to display the background of this widget.
+
+ :rtype: qt.QColor
+ """
+ return qt.QColor.fromRgbF(*self._backgroundColor)
+
+ def setBackgroundColor(self, color):
+ """Set the background color of this widget.
+
+ :param Union[List[int],List[float],QColor] color:
+ The new RGB(A) color.
+ """
+ color = colors.rgba(color)
+ if self._backgroundColor != color:
+ self._backgroundColor = color
+ self._backgroundColorsUpdated()
+
+ def getDataBackgroundColor(self):
+ """Returns the RGBA colors used to display the background of the plot
+ view displaying the data.
+
+ An invalid QColor is returned if there is no data background color.
+
+ :rtype: qt.QColor
+ """
+ if self._dataBackgroundColor is None:
+ # An invalid color
+ return qt.QColor()
+ else:
+ return qt.QColor.fromRgbF(*self._dataBackgroundColor)
+
+ def setDataBackgroundColor(self, color):
+ """Set the background color of the plot area.
+
+ Set to None or an invalid QColor to use the background color.
+
+ :param Union[List[int],List[float],QColor,None] color:
+ The new RGB(A) color.
+ """
+ if isinstance(color, qt.QColor) and not color.isValid():
+ color = None
+ if color is not None:
+ color = colors.rgba(color)
+ if self._dataBackgroundColor != color:
+ self._dataBackgroundColor = color
+ self._backgroundColorsUpdated()
+
def showEvent(self, event):
if self._autoreplot and self._dirty:
self._backend.postRedisplay()
@@ -377,16 +544,25 @@ class PlotWidget(qt.QMainWindow):
if item.isVisible():
bounds = item.getBounds()
if bounds is not None:
- xMin = numpy.nanmin([xMin, bounds[0]])
- xMax = numpy.nanmax([xMax, bounds[1]])
+ with warnings.catch_warnings():
+ warnings.simplefilter('ignore', category=RuntimeWarning)
+ # Ignore All-NaN slice encountered
+ xMin = numpy.nanmin([xMin, bounds[0]])
+ xMax = numpy.nanmax([xMax, bounds[1]])
# Take care of right axis
if (isinstance(item, items.YAxisMixIn) and
item.getYAxis() == 'right'):
- yMinRight = numpy.nanmin([yMinRight, bounds[2]])
- yMaxRight = numpy.nanmax([yMaxRight, bounds[3]])
+ with warnings.catch_warnings():
+ warnings.simplefilter('ignore', category=RuntimeWarning)
+ # Ignore All-NaN slice encountered
+ yMinRight = numpy.nanmin([yMinRight, bounds[2]])
+ yMaxRight = numpy.nanmax([yMaxRight, bounds[3]])
else:
- yMinLeft = numpy.nanmin([yMinLeft, bounds[2]])
- yMaxLeft = numpy.nanmax([yMaxLeft, bounds[3]])
+ with warnings.catch_warnings():
+ warnings.simplefilter('ignore', category=RuntimeWarning)
+ # Ignore All-NaN slice encountered
+ yMinLeft = numpy.nanmin([yMinLeft, bounds[2]])
+ yMaxLeft = numpy.nanmax([yMaxLeft, bounds[3]])
def lGetRange(x, y):
return None if numpy.isnan(x) and numpy.isnan(y) else (x, y)
@@ -528,13 +704,13 @@ class PlotWidget(qt.QMainWindow):
# This value is used when curve is updated either internally or by user.
def addCurve(self, x, y, legend=None, info=None,
- replace=False, replot=None,
+ replace=False,
color=None, symbol=None,
linewidth=None, linestyle=None,
xlabel=None, ylabel=None, yaxis=None,
xerror=None, yerror=None, z=None, selectable=None,
fill=None, resetzoom=True,
- histogram=None, copy=True, **kw):
+ histogram=None, copy=True):
"""Add a 1D curve given by x an y to the graph.
Curves are uniquely identified by their legend.
@@ -617,15 +793,6 @@ class PlotWidget(qt.QMainWindow):
False to use provided arrays.
:returns: The key string identify this curve
"""
- # Deprecation warnings
- if replot is not None:
- _logger.warning(
- 'addCurve deprecated replot argument, use resetzoom instead')
- resetzoom = replot and resetzoom
-
- if kw:
- _logger.warning('addCurve: deprecated extra arguments')
-
# This is an histogram, use addHistogram
if histogram is not None:
histoLegend = self.addHistogram(histogram=y,
@@ -825,13 +992,13 @@ class PlotWidget(qt.QMainWindow):
return legend
def addImage(self, data, legend=None, info=None,
- replace=False, replot=None,
- xScale=None, yScale=None, z=None,
+ replace=False,
+ z=None,
selectable=None, draggable=None,
colormap=None, pixmap=None,
xlabel=None, ylabel=None,
origin=None, scale=None,
- resetzoom=True, copy=True, **kw):
+ resetzoom=True, copy=True):
"""Add a 2D dataset or an image to the plot.
It displays either an array of data using a colormap or a RGB(A) image.
@@ -883,28 +1050,6 @@ class PlotWidget(qt.QMainWindow):
False to use provided arrays.
:returns: The key string identify this image
"""
- # Deprecation warnings
- if xScale is not None or yScale is not None:
- _logger.warning(
- 'addImage deprecated xScale and yScale arguments,'
- 'use origin, scale arguments instead.')
- if origin is None and scale is None:
- origin = xScale[0], yScale[0]
- scale = xScale[1], yScale[1]
- else:
- _logger.warning(
- 'addCurve: xScale, yScale and origin, scale arguments'
- ' are conflicting. xScale and yScale are ignored.'
- ' Use only origin, scale arguments.')
-
- if replot is not None:
- _logger.warning(
- 'addImage deprecated replot argument, use resetzoom instead')
- resetzoom = replot and resetzoom
-
- if kw:
- _logger.warning('addImage: deprecated extra arguments')
-
legend = "Unnamed Image 1.1" if legend is None else str(legend)
# Check if image was previously active
@@ -1090,7 +1235,8 @@ class PlotWidget(qt.QMainWindow):
def addItem(self, xdata, ydata, legend=None, info=None,
replace=False,
shape="polygon", color='black', fill=True,
- overlay=False, z=None, **kw):
+ overlay=False, z=None, linestyle="-", linewidth=1.0,
+ linebgcolor=None):
"""Add an item (i.e. a shape) to the plot.
Items are uniquely identified by their legend.
@@ -1114,13 +1260,23 @@ class PlotWidget(qt.QMainWindow):
This allows for rendering optimization if this
item is changed often.
:param int z: Layer on which to draw the item (default: 2)
+ :param str linestyle: Style of the line.
+ Only relevant for line markers where X or Y is None.
+ Value in:
+
+ - ' ' no line
+ - '-' solid line
+ - '--' dashed line
+ - '-.' dash-dot line
+ - ':' dotted line
+ :param float linewidth: Width of the line.
+ Only relevant for line markers where X or Y is None.
+ :param str linebgcolor: Background color of the line, e.g., 'blue', 'b',
+ '#FF0000'. It is used to draw dotted line using a second color.
:returns: The key string identify this item
"""
# expected to receive the same parameters as the signal
- if kw:
- _logger.warning('addItem deprecated parameters: %s', str(kw))
-
legend = "Unnamed Item 1.1" if legend is None else str(legend)
z = int(z) if z is not None else 2
@@ -1138,6 +1294,9 @@ class PlotWidget(qt.QMainWindow):
item.setOverlay(overlay)
item.setZValue(z)
item.setPoints(numpy.array((xdata, ydata)).T)
+ item.setLineStyle(linestyle)
+ item.setLineWidth(linewidth)
+ item.setLineBgColor(linebgcolor)
self._add(item)
@@ -1148,8 +1307,7 @@ class PlotWidget(qt.QMainWindow):
color=None,
selectable=False,
draggable=False,
- constraint=None,
- **kw):
+ constraint=None):
"""Add a vertical line marker to the plot.
Markers are uniquely identified by their legend.
@@ -1177,10 +1335,6 @@ class PlotWidget(qt.QMainWindow):
and that returns the filtered coordinates.
:return: The key string identify this marker
"""
- if kw:
- _logger.warning(
- 'addXMarker deprecated extra parameters: %s', str(kw))
-
return self._addMarker(x=x, y=None, legend=legend,
text=text, color=color,
selectable=selectable, draggable=draggable,
@@ -1192,8 +1346,7 @@ class PlotWidget(qt.QMainWindow):
color=None,
selectable=False,
draggable=False,
- constraint=None,
- **kw):
+ constraint=None):
"""Add a horizontal line marker to the plot.
Markers are uniquely identified by their legend.
@@ -1221,10 +1374,6 @@ class PlotWidget(qt.QMainWindow):
and that returns the filtered coordinates.
:return: The key string identify this marker
"""
- if kw:
- _logger.warning(
- 'addYMarker deprecated extra parameters: %s', str(kw))
-
return self._addMarker(x=None, y=y, legend=legend,
text=text, color=color,
selectable=selectable, draggable=draggable,
@@ -1236,8 +1385,7 @@ class PlotWidget(qt.QMainWindow):
selectable=False,
draggable=False,
symbol='+',
- constraint=None,
- **kw):
+ constraint=None):
"""Add a point marker to the plot.
Markers are uniquely identified by their legend.
@@ -1277,10 +1425,6 @@ class PlotWidget(qt.QMainWindow):
and that returns the filtered coordinates.
:return: The key string identify this marker
"""
- if kw:
- _logger.warning(
- 'addMarker deprecated extra parameters: %s', str(kw))
-
if x is None:
xmin, xmax = self._xAxis.getLimits()
x = 0.5 * (xmax + xmin)
@@ -1368,7 +1512,7 @@ class PlotWidget(qt.QMainWindow):
curve = self._getItem('curve', legend)
return curve is not None and not curve.isVisible()
- def hideCurve(self, legend, flag=True, replot=None):
+ def hideCurve(self, legend, flag=True):
"""Show/Hide the curve associated to legend.
Even when hidden, the curve is kept in the list of curves.
@@ -1376,9 +1520,6 @@ class PlotWidget(qt.QMainWindow):
:param str legend: The legend associated to the curve to be hidden
:param bool flag: True (default) to hide the curve, False to show it
"""
- if replot is not None:
- _logger.warning('hideCurve deprecated replot parameter')
-
curve = self._getItem('curve', legend)
if curve is None:
_logger.warning('Curve not in plot: %s', legend)
@@ -1660,16 +1801,13 @@ class PlotWidget(qt.QMainWindow):
return self._getActiveItem(kind='curve', just_legend=just_legend)
- def setActiveCurve(self, legend, replot=None):
+ def setActiveCurve(self, legend):
"""Make the curve associated to legend the active curve.
:param legend: The legend associated to the curve
or None to have no active curve.
:type legend: str or None
"""
- if replot is not None:
- _logger.warning('setActiveCurve deprecated replot parameter')
-
if not self.isActiveCurveHandling():
return
if legend is None and self.getActiveCurveSelectionMode() == "legacy":
@@ -1723,15 +1861,12 @@ class PlotWidget(qt.QMainWindow):
"""
return self._getActiveItem(kind='image', just_legend=just_legend)
- def setActiveImage(self, legend, replot=None):
+ def setActiveImage(self, legend):
"""Make the image associated to legend the active image.
:param str legend: The legend associated to the image
or None to have no active image.
"""
- if replot is not None:
- _logger.warning('setActiveImage deprecated replot parameter')
-
return self._setActiveItem(kind='image', legend=legend)
def _getActiveItem(self, kind, just_legend=False):
@@ -2028,14 +2163,12 @@ class PlotWidget(qt.QMainWindow):
"""
return self._backend.getGraphXLimits()
- def setGraphXLimits(self, xmin, xmax, replot=None):
+ def setGraphXLimits(self, xmin, xmax):
"""Set the graph X (bottom) limits.
:param float xmin: minimum bottom axis value
:param float xmax: maximum bottom axis value
"""
- if replot is not None:
- _logger.warning('setGraphXLimits deprecated replot parameter')
self._xAxis.setLimits(xmin, xmax)
def getGraphYLimits(self, axis='left'):
@@ -2049,7 +2182,7 @@ class PlotWidget(qt.QMainWindow):
yAxis = self._yAxis if axis == 'left' else self._yRightAxis
return yAxis.getLimits()
- def setGraphYLimits(self, ymin, ymax, axis='left', replot=None):
+ def setGraphYLimits(self, ymin, ymax, axis='left'):
"""Set the graph Y limits.
:param float ymin: minimum bottom axis value
@@ -2057,8 +2190,6 @@ class PlotWidget(qt.QMainWindow):
:param str axis: The axis for which to get the limits:
Either 'left' or 'right'
"""
- if replot is not None:
- _logger.warning('setGraphYLimits deprecated replot parameter')
assert axis in ('left', 'right')
yAxis = self._yAxis if axis == 'left' else self._yRightAxis
return yAxis.setLimits(ymin, ymax)
@@ -2192,36 +2323,6 @@ class PlotWidget(qt.QMainWindow):
def _isAxesDisplayed(self):
return self._backend.isAxesDisplayed()
- @property
- @deprecated(since_version='0.6')
- def sigSetYAxisInverted(self):
- """Signal emitted when Y axis orientation has changed"""
- return self._yAxis.sigInvertedChanged
-
- @property
- @deprecated(since_version='0.6')
- def sigSetXAxisLogarithmic(self):
- """Signal emitted when X axis scale has changed"""
- return self._xAxis._sigLogarithmicChanged
-
- @property
- @deprecated(since_version='0.6')
- def sigSetYAxisLogarithmic(self):
- """Signal emitted when Y axis scale has changed"""
- return self._yAxis._sigLogarithmicChanged
-
- @property
- @deprecated(since_version='0.6')
- def sigSetXAxisAutoScale(self):
- """Signal emitted when X axis autoscale has changed"""
- return self._xAxis.sigAutoScaleChanged
-
- @property
- @deprecated(since_version='0.6')
- def sigSetYAxisAutoScale(self):
- """Signal emitted when Y axis autoscale has changed"""
- return self._yAxis.sigAutoScaleChanged
-
def setYAxisInverted(self, flag=True):
"""Set the Y axis orientation.
@@ -2290,6 +2391,8 @@ class PlotWidget(qt.QMainWindow):
:param bool flag: True to respect data aspect ratio
"""
flag = bool(flag)
+ if flag == self.isKeepDataAspectRatio():
+ return
self._backend.setKeepDataAspectRatio(flag=flag)
self._setDirtyPlot()
self._forceResetZoom()
@@ -2323,8 +2426,8 @@ class PlotWidget(qt.QMainWindow):
# Defaults
def isDefaultPlotPoints(self):
- """Return True if default Curve symbol is 'o', False for no symbol."""
- return self._defaultPlotPoints == 'o'
+ """Return True if the default Curve symbol is set and False if not."""
+ return self._defaultPlotPoints == silx.config.DEFAULT_PLOT_SYMBOL
def setDefaultPlotPoints(self, flag):
"""Set the default symbol of all curves.
@@ -2334,7 +2437,7 @@ class PlotWidget(qt.QMainWindow):
:param bool flag: True to use 'o' as the default curve symbol,
False to use no symbol.
"""
- self._defaultPlotPoints = 'o' if flag else ''
+ self._defaultPlotPoints = silx.config.DEFAULT_PLOT_SYMBOL if flag else ''
# Reset symbol of all curves
curves = self.getAllCurves(just_legend=False, withhidden=True)
@@ -2510,7 +2613,7 @@ class PlotWidget(qt.QMainWindow):
elif ddict['event'] == 'mouseClicked' and ddict['button'] == 'left':
self.setActiveCurve(None)
- def saveGraph(self, filename, fileFormat=None, dpi=None, **kw):
+ def saveGraph(self, filename, fileFormat=None, dpi=None):
"""Save a snapshot of the plot.
Supported file formats depends on the backend in use.
@@ -2523,9 +2626,6 @@ class PlotWidget(qt.QMainWindow):
:param str fileFormat: String specifying the format
:return: False if cannot save the plot, True otherwise
"""
- if kw:
- _logger.warning('Extra parameters ignored: %s', str(kw))
-
if fileFormat is None:
if not hasattr(filename, 'lower'):
_logger.warning(
@@ -2619,9 +2719,11 @@ class PlotWidget(qt.QMainWindow):
xmin, xmax = (1., 100.) if ranges.x is None else ranges.x
ymin, ymax = (1., 100.) if ranges.y is None else ranges.y
if ranges.yright is None:
- ymin2, ymax2 = None, None
+ ymin2, ymax2 = ymin, ymax
else:
ymin2, ymax2 = ranges.yright
+ if ranges.y is None:
+ ymin, ymax = ranges.yright
# Add margins around data inside the plot area
newLimits = list(_utils.addMarginsToLimits(
@@ -3080,149 +3182,3 @@ class PlotWidget(qt.QMainWindow):
# Only call base class implementation when key is not handled.
# See QWidget.keyPressEvent for details.
super(PlotWidget, self).keyPressEvent(event)
-
- # Deprecated #
-
- def isDrawModeEnabled(self):
- """Deprecated, use :meth:`getInteractiveMode` instead.
-
- Return True if the current interactive state is drawing."""
- _logger.warning(
- 'isDrawModeEnabled deprecated, use getInteractiveMode instead')
- return self.getInteractiveMode()['mode'] == 'draw'
-
- def setDrawModeEnabled(self, flag=True, shape='polygon', label=None,
- color=None, **kwargs):
- """Deprecated, use :meth:`setInteractiveMode` instead.
-
- Set the drawing mode if flag is True and its parameters.
-
- If flag is False, only item selection is enabled.
-
- Warning: Zoom and drawing are not compatible and cannot be enabled
- simultaneously.
-
- :param bool flag: True to enable drawing and disable zoom and select.
- :param str shape: Type of item to be drawn in:
- hline, vline, rectangle, polygon (default)
- :param str label: Associated text for identifying draw signals
- :param color: The color to use to draw the selection area
- :type color: string ("#RRGGBB") or 4 column unsigned byte array or
- one of the predefined color names defined in colors.py
- """
- _logger.warning(
- 'setDrawModeEnabled deprecated, use setInteractiveMode instead')
-
- if kwargs:
- _logger.warning('setDrawModeEnabled ignores additional parameters')
-
- if color is None:
- color = 'black'
-
- if flag:
- self.setInteractiveMode('draw', shape=shape,
- label=label, color=color)
- elif self.getInteractiveMode()['mode'] == 'draw':
- self.setInteractiveMode('select')
-
- def getDrawMode(self):
- """Deprecated, use :meth:`getInteractiveMode` instead.
-
- Return the draw mode parameters as a dict of None.
-
- It returns None if the interactive mode is not a drawing mode,
- otherwise, it returns a dict containing the drawing mode parameters
- as provided to :meth:`setDrawModeEnabled`.
- """
- _logger.warning(
- 'getDrawMode deprecated, use getInteractiveMode instead')
- mode = self.getInteractiveMode()
- return mode if mode['mode'] == 'draw' else None
-
- def isZoomModeEnabled(self):
- """Deprecated, use :meth:`getInteractiveMode` instead.
-
- Return True if the current interactive state is zooming."""
- _logger.warning(
- 'isZoomModeEnabled deprecated, use getInteractiveMode instead')
- return self.getInteractiveMode()['mode'] == 'zoom'
-
- def setZoomModeEnabled(self, flag=True, color=None):
- """Deprecated, use :meth:`setInteractiveMode` instead.
-
- Set the zoom mode if flag is True, else item selection is enabled.
-
- Warning: Zoom and drawing are not compatible and cannot be enabled
- simultaneously
-
- :param bool flag: If True, enable zoom and select mode.
- :param color: The color to use to draw the selection area.
- (Default: 'black')
- :param color: The color to use to draw the selection area
- :type color: string ("#RRGGBB") or 4 column unsigned byte array or
- one of the predefined color names defined in colors.py
- """
- _logger.warning(
- 'setZoomModeEnabled deprecated, use setInteractiveMode instead')
- if color is None:
- color = 'black'
-
- if flag:
- self.setInteractiveMode('zoom', color=color)
- elif self.getInteractiveMode()['mode'] == 'zoom':
- self.setInteractiveMode('select')
-
- def insertMarker(self, *args, **kwargs):
- """Deprecated, use :meth:`addMarker` instead."""
- _logger.warning(
- 'insertMarker deprecated, use addMarker instead.')
- return self.addMarker(*args, **kwargs)
-
- def insertXMarker(self, *args, **kwargs):
- """Deprecated, use :meth:`addXMarker` instead."""
- _logger.warning(
- 'insertXMarker deprecated, use addXMarker instead.')
- return self.addXMarker(*args, **kwargs)
-
- def insertYMarker(self, *args, **kwargs):
- """Deprecated, use :meth:`addYMarker` instead."""
- _logger.warning(
- 'insertYMarker deprecated, use addYMarker instead.')
- return self.addYMarker(*args, **kwargs)
-
- def isActiveCurveHandlingEnabled(self):
- """Deprecated, use :meth:`isActiveCurveHandling` instead."""
- _logger.warning(
- 'isActiveCurveHandlingEnabled deprecated, '
- 'use isActiveCurveHandling instead.')
- return self.isActiveCurveHandling()
-
- def enableActiveCurveHandling(self, *args, **kwargs):
- """Deprecated, use :meth:`setActiveCurveHandling` instead."""
- _logger.warning(
- 'enableActiveCurveHandling deprecated, '
- 'use setActiveCurveHandling instead.')
- return self.setActiveCurveHandling(*args, **kwargs)
-
- def invertYAxis(self, *args, **kwargs):
- """Deprecated, use :meth:`Axis.setInverted` instead."""
- _logger.warning('invertYAxis deprecated, '
- 'use getYAxis().setInverted instead.')
- return self.getYAxis().setInverted(*args, **kwargs)
-
- def showGrid(self, flag=True):
- """Deprecated, use :meth:`setGraphGrid` instead."""
- _logger.warning("showGrid deprecated, use setGraphGrid instead")
- if flag in (0, False):
- flag = None
- elif flag in (1, True):
- flag = 'major'
- else:
- flag = 'both'
- return self.setGraphGrid(flag)
-
- def keepDataAspectRatio(self, *args, **kwargs):
- """Deprecated, use :meth:`setKeepDataAspectRatio`."""
- _logger.warning('keepDataAspectRatio deprecated,'
- 'use setKeepDataAspectRatio instead')
- return self.setKeepDataAspectRatio(*args, **kwargs)
diff --git a/silx/gui/plot/PlotWindow.py b/silx/gui/plot/PlotWindow.py
index 23ea399..a39430e 100644
--- a/silx/gui/plot/PlotWindow.py
+++ b/silx/gui/plot/PlotWindow.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,15 +29,19 @@ The :class:`PlotWindow` is a subclass of :class:`.PlotWidget`.
__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
-__date__ = "12/10/2018"
+__date__ = "12/04/2019"
-import collections
+try:
+ from collections import abc
+except ImportError: # Python2 support
+ import collections as abc
import logging
import weakref
import silx
from silx.utils.weakref import WeakMethodProxy
from silx.utils.deprecation import deprecated
+from silx.utils.proxy import docstring
from . import PlotWidget
from . import actions
@@ -128,53 +132,53 @@ class PlotWindow(PlotWidget):
self.group.setExclusive(False)
self.resetZoomAction = self.group.addAction(
- actions.control.ResetZoomAction(self))
+ actions.control.ResetZoomAction(self, parent=self))
self.resetZoomAction.setVisible(resetzoom)
self.addAction(self.resetZoomAction)
- self.zoomInAction = actions.control.ZoomInAction(self)
+ self.zoomInAction = actions.control.ZoomInAction(self, parent=self)
self.addAction(self.zoomInAction)
- self.zoomOutAction = actions.control.ZoomOutAction(self)
+ self.zoomOutAction = actions.control.ZoomOutAction(self, parent=self)
self.addAction(self.zoomOutAction)
self.xAxisAutoScaleAction = self.group.addAction(
- actions.control.XAxisAutoScaleAction(self))
+ actions.control.XAxisAutoScaleAction(self, parent=self))
self.xAxisAutoScaleAction.setVisible(autoScale)
self.addAction(self.xAxisAutoScaleAction)
self.yAxisAutoScaleAction = self.group.addAction(
- actions.control.YAxisAutoScaleAction(self))
+ actions.control.YAxisAutoScaleAction(self, parent=self))
self.yAxisAutoScaleAction.setVisible(autoScale)
self.addAction(self.yAxisAutoScaleAction)
self.xAxisLogarithmicAction = self.group.addAction(
- actions.control.XAxisLogarithmicAction(self))
+ actions.control.XAxisLogarithmicAction(self, parent=self))
self.xAxisLogarithmicAction.setVisible(logScale)
self.addAction(self.xAxisLogarithmicAction)
self.yAxisLogarithmicAction = self.group.addAction(
- actions.control.YAxisLogarithmicAction(self))
+ actions.control.YAxisLogarithmicAction(self, parent=self))
self.yAxisLogarithmicAction.setVisible(logScale)
self.addAction(self.yAxisLogarithmicAction)
self.gridAction = self.group.addAction(
- actions.control.GridAction(self, gridMode='both'))
+ actions.control.GridAction(self, gridMode='both', parent=self))
self.gridAction.setVisible(grid)
self.addAction(self.gridAction)
self.curveStyleAction = self.group.addAction(
- actions.control.CurveStyleAction(self))
+ actions.control.CurveStyleAction(self, parent=self))
self.curveStyleAction.setVisible(curveStyle)
self.addAction(self.curveStyleAction)
self.colormapAction = self.group.addAction(
- actions.control.ColormapAction(self))
+ actions.control.ColormapAction(self, parent=self))
self.colormapAction.setVisible(colormap)
self.addAction(self.colormapAction)
self.colorbarAction = self.group.addAction(
- actions_control.ColorBarAction(self, self))
+ actions_control.ColorBarAction(self, parent=self))
self.colorbarAction.setVisible(False)
self.addAction(self.colorbarAction)
self._colorbar.setVisible(False)
@@ -194,18 +198,18 @@ class PlotWindow(PlotWidget):
self.getMaskAction().setVisible(mask)
self._intensityHistoAction = self.group.addAction(
- actions_histogram.PixelIntensitiesHistoAction(self))
+ actions_histogram.PixelIntensitiesHistoAction(self, parent=self))
self._intensityHistoAction.setVisible(False)
self._medianFilter2DAction = self.group.addAction(
- actions_medfilt.MedianFilter2DAction(self))
+ actions_medfilt.MedianFilter2DAction(self, parent=self))
self._medianFilter2DAction.setVisible(False)
self._medianFilter1DAction = self.group.addAction(
- actions_medfilt.MedianFilter1DAction(self))
+ actions_medfilt.MedianFilter1DAction(self, parent=self))
self._medianFilter1DAction.setVisible(False)
- self.fitAction = self.group.addAction(actions_fit.FitAction(self))
+ self.fitAction = self.group.addAction(actions_fit.FitAction(self, parent=self))
self.fitAction.setVisible(fit)
self.addAction(self.fitAction)
@@ -217,10 +221,8 @@ class PlotWindow(PlotWidget):
# Make colorbar background white
self._colorbar.setAutoFillBackground(True)
- palette = self._colorbar.palette()
- palette.setColor(qt.QPalette.Background, qt.Qt.white)
- palette.setColor(qt.QPalette.Window, qt.Qt.white)
- self._colorbar.setPalette(palette)
+ self._sigAxesVisibilityChanged.connect(self._updateColorBarBackground)
+ self._updateColorBarBackground()
gridLayout = qt.QGridLayout()
gridLayout.setSpacing(0)
@@ -252,7 +254,7 @@ class PlotWindow(PlotWidget):
hbox.addWidget(self.controlButton)
if position: # Add PositionInfo widget to the bottom of the plot
- if isinstance(position, collections.Iterable):
+ if isinstance(position, abc.Iterable):
# Use position as a set of converters
converters = position
else:
@@ -280,7 +282,7 @@ class PlotWindow(PlotWidget):
parent=self, plot=self)
self.addToolBar(self._interactiveModeToolBar)
- self._toolbar = self._createToolBar(title='Plot', parent=None)
+ self._toolbar = self._createToolBar(title='Plot', parent=self)
self.addToolBar(self._toolbar)
self._outputToolBar = tools.OutputToolBar(parent=self, plot=self)
@@ -294,6 +296,40 @@ class PlotWindow(PlotWidget):
for action in toolbar.actions():
self.addAction(action)
+ @docstring(PlotWidget)
+ def setBackgroundColor(self, color):
+ super(PlotWindow, self).setBackgroundColor(color)
+ self._updateColorBarBackground()
+
+ @docstring(PlotWidget)
+ def setDataBackgroundColor(self, color):
+ super(PlotWindow, self).setDataBackgroundColor(color)
+ self._updateColorBarBackground()
+
+ @docstring(PlotWidget)
+ def setForegroundColor(self, color):
+ super(PlotWindow, self).setForegroundColor(color)
+ self._updateColorBarBackground()
+
+ def _updateColorBarBackground(self):
+ """Update the colorbar background according to the state of the plot"""
+ if self._isAxesDisplayed():
+ color = self.getBackgroundColor()
+ else:
+ color = self.getDataBackgroundColor()
+ if not color.isValid():
+ # If no color defined, use the background one
+ color = self.getBackgroundColor()
+
+ foreground = self.getForegroundColor()
+
+ palette = self._colorbar.palette()
+ palette.setColor(qt.QPalette.Background, color)
+ palette.setColor(qt.QPalette.Window, color)
+ palette.setColor(qt.QPalette.WindowText, foreground)
+ palette.setColor(qt.QPalette.Text, foreground)
+ self._colorbar.setPalette(palette)
+
def getInteractiveModeToolBar(self):
"""Returns QToolBar controlling interactive mode.
@@ -457,10 +493,6 @@ class PlotWindow(PlotWidget):
return self._colorbar
# getters for dock widgets
- @property
- @deprecated(replacement="getLegendsDockWidget()", since_version="0.4.0")
- def legendsDockWidget(self):
- return self.getLegendsDockWidget()
def getLegendsDockWidget(self):
"""DockWidget with Legend panel"""
@@ -470,11 +502,6 @@ class PlotWindow(PlotWidget):
self.addTabbedDockWidget(self._legendsDockWidget)
return self._legendsDockWidget
- @property
- @deprecated(replacement="getCurvesRoiWidget()", since_version="0.4.0")
- def curvesROIDockWidget(self):
- return self.getCurvesRoiDockWidget()
-
def getCurvesRoiDockWidget(self):
# Undocumented for a "soft deprecation" in version 0.7.0
# (still used internally for lazy loading)
@@ -496,11 +523,6 @@ class PlotWindow(PlotWidget):
"""
return self.getCurvesRoiDockWidget().roiWidget
- @property
- @deprecated(replacement="getMaskToolsDockWidget()", since_version="0.4.0")
- def maskToolsDockWidget(self):
- return self.getMaskToolsDockWidget()
-
def getMaskToolsDockWidget(self):
"""DockWidget with image mask panel (lazy-loaded)."""
if self._maskToolsDockWidget is None:
@@ -539,11 +561,6 @@ class PlotWindow(PlotWidget):
def panModeAction(self):
return self.getInteractiveModeToolBar().getPanModeAction()
- @property
- @deprecated(replacement="getConsoleAction()", since_version="0.4.0")
- def consoleAction(self):
- return self.getConsoleAction()
-
def getConsoleAction(self):
"""QAction handling the IPython console activation.
@@ -563,11 +580,6 @@ class PlotWindow(PlotWidget):
self._consoleAction.setEnabled(False)
return self._consoleAction
- @property
- @deprecated(replacement="getCrosshairAction()", since_version="0.4.0")
- def crosshairAction(self):
- return self.getCrosshairAction()
-
def getCrosshairAction(self):
"""Action toggling crosshair cursor mode.
@@ -577,11 +589,6 @@ class PlotWindow(PlotWidget):
self._crosshairAction = actions.control.CrosshairAction(self, color='red')
return self._crosshairAction
- @property
- @deprecated(replacement="getMaskAction()", since_version="0.4.0")
- def maskAction(self):
- return self.getMaskAction()
-
def getMaskAction(self):
"""QAction toggling image mask dock widget
@@ -589,12 +596,6 @@ class PlotWindow(PlotWidget):
"""
return self.getMaskToolsDockWidget().toggleViewAction()
- @property
- @deprecated(replacement="getPanWithArrowKeysAction()",
- since_version="0.4.0")
- def panWithArrowKeysAction(self):
- return self.getPanWithArrowKeysAction()
-
def getPanWithArrowKeysAction(self):
"""Action toggling pan with arrow keys.
@@ -604,11 +605,6 @@ class PlotWindow(PlotWidget):
self._panWithArrowKeysAction = actions.control.PanWithArrowKeysAction(self)
return self._panWithArrowKeysAction
- @property
- @deprecated(replacement="getRoiAction()", since_version="0.4.0")
- def roiAction(self):
- return self.getRoiAction()
-
def getStatsAction(self):
if self._statsAction is None:
self._statsAction = qt.QAction('Curves stats', self)
@@ -829,7 +825,9 @@ class Plot2D(PlotWindow):
posInfo = [
('X', lambda x, y: x),
('Y', lambda x, y: y),
- ('Data', WeakMethodProxy(self._getImageValue))]
+ ('Data', WeakMethodProxy(self._getImageValue)),
+ ('Dims', WeakMethodProxy(self._getImageDims)),
+ ]
super(Plot2D, self).__init__(parent=parent, backend=backend,
resetzoom=True, autoScale=False,
@@ -929,6 +927,15 @@ class Plot2D(PlotWindow):
return value, "Masked"
return value
+ def _getImageDims(self, *args):
+ activeImage = self.getActiveImage()
+ if (activeImage is not None and
+ activeImage.getData(copy=False) is not None):
+ dims = activeImage.getData(copy=False).shape[1::-1]
+ return 'x'.join(str(dim) for dim in dims)
+ else:
+ return '-'
+
def getProfileToolbar(self):
"""Profile tools attached to this plot
diff --git a/silx/gui/plot/PrintPreviewToolButton.py b/silx/gui/plot/PrintPreviewToolButton.py
index b48505d..d857c18 100644
--- a/silx/gui/plot/PrintPreviewToolButton.py
+++ b/silx/gui/plot/PrintPreviewToolButton.py
@@ -111,10 +111,11 @@ from .. import icons
from . import PlotWidget
from ..widgets.PrintPreview import PrintPreviewDialog, SingletonPrintPreviewDialog
from ..widgets.PrintGeometryDialog import PrintGeometryDialog
+from silx.utils.deprecation import deprecated
__authors__ = ["P. Knobel"]
__license__ = "MIT"
-__date__ = "18/07/2017"
+__date__ = "20/12/2018"
_logger = logging.getLogger(__name__)
# _logger.setLevel(logging.DEBUG)
@@ -132,19 +133,19 @@ class PrintPreviewToolButton(qt.QToolButton):
if not isinstance(plot, PlotWidget):
raise TypeError("plot parameter must be a PlotWidget")
- self.plot = plot
+ self._plot = plot
self.setIcon(icons.getQIcon('document-print'))
printGeomAction = qt.QAction("Print geometry", self)
printGeomAction.setToolTip("Define a print geometry prior to sending "
"the plot to the print preview dialog")
- printGeomAction.setIcon(icons.getQIcon('shape-rectangle')) # fixme: icon not displayed in menu
+ printGeomAction.setIcon(icons.getQIcon('shape-rectangle'))
printGeomAction.triggered.connect(self._setPrintConfiguration)
printPreviewAction = qt.QAction("Print preview", self)
printPreviewAction.setToolTip("Send plot to the print preview dialog")
- printPreviewAction.setIcon(icons.getQIcon('document-print')) # fixme: icon not displayed
+ printPreviewAction.setIcon(icons.getQIcon('document-print'))
printPreviewAction.triggered.connect(self._plotToPrintPreview)
menu = qt.QMenu(self)
@@ -172,24 +173,64 @@ class PrintPreviewToolButton(qt.QToolButton):
self._printPreviewDialog = PrintPreviewDialog(self.parent())
return self._printPreviewDialog
+ def getTitle(self):
+ """Implement this method to fetch the title in the plot.
+
+ :return: Title to be printed above the plot, or None (no title added)
+ :rtype: str or None
+ """
+ return None
+
+ def getCommentAndPosition(self):
+ """Implement this method to fetch the legend to be printed below the
+ figure and its position.
+
+ :return: Legend to be printed below the figure and its position:
+ "CENTER", "LEFT" or "RIGHT"
+ :rtype: (str, str) or (None, None)
+ """
+ return None, None
+
+ @property
+ @deprecated(since_version="0.10",
+ replacement="getPlot()")
+ def plot(self):
+ return self._plot
+
+ def getPlot(self):
+ """Return the :class:`.PlotWidget` associated with this tool button.
+
+ :rtype: :class:`.PlotWidget`
+ """
+ return self._plot
+
def _plotToPrintPreview(self):
"""Grab the plot widget and send it to the print preview dialog.
Make sure the print preview dialog is shown and raised."""
if not self.printPreviewDialog.ensurePrinterIsSet():
return
+ comment, commentPosition = self.getCommentAndPosition()
+
if qt.HAS_SVG:
svgRenderer, viewBox = self._getSvgRendererAndViewbox()
self.printPreviewDialog.addSvgItem(svgRenderer,
- viewBox=viewBox)
+ title=self.getTitle(),
+ comment=comment,
+ commentPosition=commentPosition,
+ viewBox=viewBox,
+ keepRatio=self._printGeometry["keepAspectRatio"])
else:
_logger.warning("Missing QtSvg library, using a raster image")
if qt.BINDING in ["PyQt4", "PySide"]:
- pixmap = qt.QPixmap.grabWidget(self.plot.centralWidget())
+ pixmap = qt.QPixmap.grabWidget(self._plot.centralWidget())
else:
# PyQt5 and hopefully PyQt6+
- pixmap = self.plot.centralWidget().grab()
- self.printPreviewDialog.addPixmap(pixmap)
+ pixmap = self._plot.centralWidget().grab()
+ self.printPreviewDialog.addPixmap(pixmap,
+ title=self.getTitle(),
+ comment=comment,
+ commentPosition=commentPosition)
self.printPreviewDialog.show()
self.printPreviewDialog.raise_()
@@ -201,7 +242,7 @@ class PrintPreviewToolButton(qt.QToolButton):
and to the geometry configuration (width, height, ratio) specified
by the user."""
imgData = StringIO()
- assert self.plot.saveGraph(imgData, fileFormat="svg"), \
+ assert self._plot.saveGraph(imgData, fileFormat="svg"), \
"Unable to save graph"
imgData.flush()
imgData.seek(0)
@@ -310,7 +351,7 @@ class PrintPreviewToolButton(qt.QToolButton):
self._printGeometry = self._printConfigurationDialog.getPrintGeometry()
def _getPlotAspectRatio(self):
- widget = self.plot.centralWidget()
+ widget = self._plot.centralWidget()
graphWidth = float(widget.width())
graphHeight = float(widget.height())
return graphHeight / graphWidth
diff --git a/silx/gui/plot/Profile.py b/silx/gui/plot/Profile.py
index 182cf60..e2aa5a7 100644
--- a/silx/gui/plot/Profile.py
+++ b/silx/gui/plot/Profile.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -28,7 +28,7 @@ and stacks of images"""
__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel", "H. Payno"]
__license__ = "MIT"
-__date__ = "24/07/2018"
+__date__ = "12/04/2019"
import weakref
@@ -180,7 +180,8 @@ def createProfile(roiInfo, currentData, origin, scale, lineWidth, method):
:type scale: 2-tuple of float
:param int lineWidth: width of the profile line
:param str method: method to compute the profile. Can be 'mean' or 'sum'
- :return: `profile, area, profileName, xLabel`, where:
+ :return: `coords, profile, area, profileName, xLabel`, where:
+ - coords is the X coordinate to use to display the profile
- profile is a 2D array of the profiles of the stack of images.
For a single image, the profile is a curve, so this parameter
has a shape *(1, len(curve))*
@@ -188,10 +189,9 @@ def createProfile(roiInfo, currentData, origin, scale, lineWidth, method):
the effective ROI area corners in plot coords.
- profileName is a string describing the ROI, meant to be used as
title of the profile plot
- - xLabel is a string describing the meaning of the X axis on the
- profile plot ("rows", "columns", "distance")
+ - xLabel the label for X in the profile window
- :rtype: tuple(ndarray, (ndarray, ndarray), str, str)
+ :rtype: tuple(ndarray,ndarray,(ndarray,ndarray),str)
"""
if currentData is None or roiInfo is None or lineWidth is None:
raise ValueError("createProfile called with invalide arguments")
@@ -212,12 +212,15 @@ def createProfile(roiInfo, currentData, origin, scale, lineWidth, method):
axis=0,
method=method)
+ coords = numpy.arange(len(profile[0]), dtype=numpy.float32)
+ coords = coords * scale[0] + origin[0]
+
yMin, yMax = min(area[1]), max(area[1]) - 1
if roiWidth <= 1:
profileName = 'Y = %g' % yMin
else:
profileName = 'Y = [%g, %g]' % (yMin, yMax)
- xLabel = 'Columns'
+ xLabel = 'X'
elif lineProjectionMode == 'Y': # Vertical profile on the whole image
profile, area = _alignedFullProfile(currentData3D,
@@ -226,12 +229,15 @@ def createProfile(roiInfo, currentData, origin, scale, lineWidth, method):
axis=1,
method=method)
+ coords = numpy.arange(len(profile[0]), dtype=numpy.float32)
+ coords = coords * scale[1] + origin[1]
+
xMin, xMax = min(area[0]), max(area[0]) - 1
if roiWidth <= 1:
profileName = 'X = %g' % xMin
else:
profileName = 'X = [%g, %g]' % (xMin, xMax)
- xLabel = 'Rows'
+ xLabel = 'Y'
else: # Free line profile
@@ -306,35 +312,52 @@ def createProfile(roiInfo, currentData, origin, scale, lineWidth, method):
dCol = (endPt[1] - startPt[1]) / length
# Extend ROI with half a pixel on each end
- startPt = startPt[0] - 0.5 * dRow, startPt[1] - 0.5 * dCol
- endPt = endPt[0] + 0.5 * dRow, endPt[1] + 0.5 * dCol
+ roiStartPt = startPt[0] - 0.5 * dRow, startPt[1] - 0.5 * dCol
+ roiEndPt = endPt[0] + 0.5 * dRow, endPt[1] + 0.5 * dCol
# Rotate deltas by 90 degrees to apply line width
dRow, dCol = dCol, -dRow
area = (
- numpy.array((startPt[1] - 0.5 * roiWidth * dCol,
- startPt[1] + 0.5 * roiWidth * dCol,
- endPt[1] + 0.5 * roiWidth * dCol,
- endPt[1] - 0.5 * roiWidth * dCol),
+ numpy.array((roiStartPt[1] - 0.5 * roiWidth * dCol,
+ roiStartPt[1] + 0.5 * roiWidth * dCol,
+ roiEndPt[1] + 0.5 * roiWidth * dCol,
+ roiEndPt[1] - 0.5 * roiWidth * dCol),
dtype=numpy.float32) * scale[0] + origin[0],
- numpy.array((startPt[0] - 0.5 * roiWidth * dRow,
- startPt[0] + 0.5 * roiWidth * dRow,
- endPt[0] + 0.5 * roiWidth * dRow,
- endPt[0] - 0.5 * roiWidth * dRow),
+ numpy.array((roiStartPt[0] - 0.5 * roiWidth * dRow,
+ roiStartPt[0] + 0.5 * roiWidth * dRow,
+ roiEndPt[0] + 0.5 * roiWidth * dRow,
+ roiEndPt[0] - 0.5 * roiWidth * dRow),
dtype=numpy.float32) * scale[1] + origin[1])
- y0, x0 = startPt
- y1, x1 = endPt
- if x1 == x0 or y1 ==