summaryrefslogtreecommitdiff
path: root/silx
diff options
context:
space:
mode:
authorPicca Frédéric-Emmanuel <picca@debian.org>2017-10-07 07:59:01 +0200
committerPicca Frédéric-Emmanuel <picca@debian.org>2017-10-07 07:59:01 +0200
commitbfa4dba15485b4192f8bbe13345e9658c97ecf76 (patch)
treefb9c6e5860881fbde902f7cbdbd41dc4a3a9fb5d /silx
parentf7bdc2acff3c13a6d632c28c4569690ab106eed7 (diff)
New upstream version 0.6.0+dfsg
Diffstat (limited to 'silx')
-rw-r--r--silx/__init__.py7
-rw-r--r--silx/__main__.py16
-rw-r--r--silx/app/convert.py283
-rw-r--r--silx/app/qtutils.py243
-rw-r--r--silx/app/test/__init__.py10
-rw-r--r--silx/app/test/test_convert.py182
-rw-r--r--silx/app/test/test_view.py33
-rw-r--r--silx/app/test_.py175
-rw-r--r--silx/app/view.py184
-rw-r--r--silx/gui/_glutils/FramebufferTexture.py78
-rw-r--r--silx/gui/_glutils/OpenGLWidget.py409
-rw-r--r--silx/gui/_glutils/VertexBuffer.py10
-rw-r--r--silx/gui/_glutils/__init__.py1
-rw-r--r--silx/gui/_glutils/font.py32
-rw-r--r--silx/gui/console.py4
-rw-r--r--silx/gui/data/ArrayTableModel.py7
-rw-r--r--silx/gui/data/ArrayTableWidget.py2
-rw-r--r--silx/gui/data/DataViewer.py10
-rw-r--r--silx/gui/data/DataViewerFrame.py12
-rw-r--r--silx/gui/data/DataViews.py209
-rw-r--r--silx/gui/data/Hdf5TableView.py76
-rw-r--r--silx/gui/data/HexaTableView.py278
-rw-r--r--silx/gui/data/NXdataWidgets.py22
-rw-r--r--silx/gui/data/RecordTableView.py10
-rw-r--r--silx/gui/data/TextFormatter.py168
-rw-r--r--silx/gui/data/test/test_dataviewer.py28
-rw-r--r--silx/gui/data/test/test_textformatter.py113
-rw-r--r--silx/gui/fit/BackgroundWidget.py16
-rw-r--r--silx/gui/fit/FitConfig.py6
-rw-r--r--silx/gui/fit/FitWidget.py2
-rw-r--r--silx/gui/hdf5/Hdf5Formatter.py229
-rw-r--r--silx/gui/hdf5/Hdf5HeaderView.py29
-rw-r--r--silx/gui/hdf5/Hdf5Item.py182
-rw-r--r--silx/gui/hdf5/Hdf5Node.py29
-rw-r--r--silx/gui/hdf5/Hdf5TreeModel.py78
-rw-r--r--silx/gui/hdf5/Hdf5TreeView.py85
-rw-r--r--silx/gui/hdf5/NexusSortFilterProxyModel.py5
-rw-r--r--silx/gui/hdf5/_utils.py184
-rw-r--r--silx/gui/hdf5/test/_mock.py130
-rw-r--r--silx/gui/hdf5/test/test_hdf5.py454
-rw-r--r--silx/gui/icons.py54
-rw-r--r--silx/gui/plot/ColorBar.py456
-rw-r--r--silx/gui/plot/Colormap.py410
-rw-r--r--silx/gui/plot/ColormapDialog.py78
-rw-r--r--silx/gui/plot/Colors.py217
-rw-r--r--silx/gui/plot/ComplexImageView.py670
-rw-r--r--silx/gui/plot/CurvesROIWidget.py20
-rw-r--r--silx/gui/plot/ImageView.py157
-rw-r--r--silx/gui/plot/ItemsSelectionDialog.py282
-rw-r--r--silx/gui/plot/LegendSelector.py21
-rw-r--r--silx/gui/plot/LimitsHistory.py83
-rw-r--r--silx/gui/plot/MPLColormap.py1062
-rw-r--r--silx/gui/plot/MaskToolsWidget.py87
-rw-r--r--silx/gui/plot/Plot.py2925
-rw-r--r--silx/gui/plot/PlotActions.py1399
-rw-r--r--silx/gui/plot/PlotInteraction.py79
-rw-r--r--silx/gui/plot/PlotToolButtons.py13
-rw-r--r--silx/gui/plot/PlotTools.py152
-rw-r--r--silx/gui/plot/PlotWidget.py3020
-rw-r--r--silx/gui/plot/PlotWindow.py193
-rw-r--r--silx/gui/plot/PrintPreviewToolButton.py350
-rw-r--r--silx/gui/plot/Profile.py77
-rw-r--r--silx/gui/plot/ScatterMaskToolsWidget.py13
-rw-r--r--silx/gui/plot/StackView.py479
-rw-r--r--silx/gui/plot/_BaseMaskToolsWidget.py124
-rw-r--r--silx/gui/plot/__init__.py7
-rw-r--r--silx/gui/plot/_utils/__init__.py11
-rw-r--r--silx/gui/plot/_utils/panzoom.py154
-rw-r--r--silx/gui/plot/actions/PlotAction.py79
-rw-r--r--silx/gui/plot/actions/__init__.py38
-rw-r--r--silx/gui/plot/actions/control.py549
-rw-r--r--silx/gui/plot/actions/fit.py189
-rw-r--r--silx/gui/plot/actions/histogram.py170
-rw-r--r--silx/gui/plot/actions/io.py538
-rw-r--r--silx/gui/plot/actions/medfilt.py145
-rw-r--r--silx/gui/plot/actions/mode.py100
-rw-r--r--silx/gui/plot/backends/BackendBase.py39
-rw-r--r--silx/gui/plot/backends/BackendMatplotlib.py104
-rw-r--r--silx/gui/plot/backends/BackendOpenGL.py144
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotCurve.py2
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotFrame.py24
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotImage.py56
-rw-r--r--silx/gui/plot/backends/glutils/GLText.py43
-rw-r--r--silx/gui/plot/backends/glutils/PlotImageFile.py14
-rw-r--r--silx/gui/plot/items/__init__.py13
-rw-r--r--silx/gui/plot/items/axis.py477
-rw-r--r--silx/gui/plot/items/core.py182
-rw-r--r--silx/gui/plot/items/curve.py18
-rw-r--r--silx/gui/plot/items/histogram.py36
-rw-r--r--silx/gui/plot/items/image.py75
-rw-r--r--silx/gui/plot/items/marker.py11
-rw-r--r--silx/gui/plot/items/scatter.py10
-rw-r--r--silx/gui/plot/items/shape.py14
-rw-r--r--silx/gui/plot/matplotlib/Colormap.py282
-rw-r--r--silx/gui/plot/matplotlib/ModestImage.py (renamed from silx/gui/plot/backends/ModestImage.py)2
-rw-r--r--silx/gui/plot/matplotlib/__init__.py (renamed from silx/gui/plot/backends/_matplotlib.py)14
-rw-r--r--silx/gui/plot/setup.py5
-rw-r--r--silx/gui/plot/test/__init__.py80
-rw-r--r--silx/gui/plot/test/testColorBar.py227
-rw-r--r--silx/gui/plot/test/testColormap.py291
-rw-r--r--silx/gui/plot/test/testColors.py8
-rw-r--r--silx/gui/plot/test/testComplexImageView.py95
-rw-r--r--silx/gui/plot/test/testCurvesROIWidget.py3
-rw-r--r--silx/gui/plot/test/testItem.py231
-rw-r--r--silx/gui/plot/test/testLegendSelector.py3
-rw-r--r--silx/gui/plot/test/testLimitConstraints.py125
-rw-r--r--silx/gui/plot/test/testMaskToolsWidget.py38
-rw-r--r--silx/gui/plot/test/testPlotInteraction.py27
-rw-r--r--silx/gui/plot/test/testPlotTools.py23
-rw-r--r--silx/gui/plot/test/testPlotWidget.py775
-rw-r--r--silx/gui/plot/test/testPlotWidgetNoBackend.py (renamed from silx/gui/plot/test/testPlot.py)98
-rw-r--r--silx/gui/plot/test/testPlotWindow.py14
-rw-r--r--silx/gui/plot/test/testScatterMaskToolsWidget.py37
-rw-r--r--silx/gui/plot/test/testStackView.py33
-rw-r--r--silx/gui/plot/test/testUtilsAxis.py148
-rw-r--r--silx/gui/plot/test/utils.py194
-rw-r--r--silx/gui/plot/utils/__init__.py30
-rw-r--r--silx/gui/plot/utils/axis.py164
-rw-r--r--silx/gui/plot3d/Plot3DWidget.py203
-rw-r--r--silx/gui/plot3d/Plot3DWindow.py20
-rw-r--r--silx/gui/plot3d/SFViewParamTree.py142
-rw-r--r--silx/gui/plot3d/ScalarFieldView.py270
-rw-r--r--silx/gui/plot3d/__init__.py7
-rw-r--r--silx/gui/plot3d/actions/Plot3DAction.py69
-rw-r--r--silx/gui/plot3d/actions/__init__.py33
-rw-r--r--silx/gui/plot3d/actions/io.py (renamed from silx/gui/plot3d/Plot3DActions.py)44
-rw-r--r--silx/gui/plot3d/actions/mode.py126
-rw-r--r--silx/gui/plot3d/scene/axes.py19
-rw-r--r--silx/gui/plot3d/scene/function.py128
-rw-r--r--silx/gui/plot3d/scene/interaction.py29
-rw-r--r--silx/gui/plot3d/scene/primitives.py7
-rw-r--r--silx/gui/plot3d/scene/viewport.py15
-rw-r--r--silx/gui/plot3d/scene/window.py11
-rw-r--r--silx/gui/plot3d/setup.py2
-rw-r--r--silx/gui/plot3d/test/__init__.py4
-rw-r--r--silx/gui/plot3d/test/testGL.py84
-rw-r--r--silx/gui/plot3d/test/testScalarFieldView.py114
-rw-r--r--silx/gui/plot3d/tools/ViewpointTools.py (renamed from silx/gui/plot3d/ViewpointToolBar.py)29
-rw-r--r--silx/gui/plot3d/tools/__init__.py32
-rw-r--r--silx/gui/plot3d/tools/toolbars.py (renamed from silx/gui/plot3d/Plot3DToolBar.py)85
-rw-r--r--silx/gui/setup.py2
-rw-r--r--silx/gui/test/test_icons.py56
-rw-r--r--silx/gui/test/utils.py36
-rw-r--r--silx/gui/widgets/FloatEdit.py65
-rw-r--r--silx/gui/widgets/FrameBrowser.py4
-rw-r--r--silx/gui/widgets/PeriodicTable.py6
-rw-r--r--silx/gui/widgets/PrintGeometryDialog.py222
-rw-r--r--silx/gui/widgets/PrintPreview.py704
-rw-r--r--silx/gui/widgets/TableWidget.py162
-rw-r--r--silx/gui/widgets/ThreadPoolPushButton.py2
-rw-r--r--silx/gui/widgets/WaitingPushButton.py2
-rw-r--r--silx/gui/widgets/test/__init__.py4
-rw-r--r--silx/gui/widgets/test/test_printpreview.py74
-rw-r--r--silx/image/backprojection.py25
-rw-r--r--silx/image/bilinear.c9557
-rw-r--r--silx/image/bilinear.pyx2
-rw-r--r--silx/image/phantomgenerator.py160
-rw-r--r--silx/image/projection.py25
-rw-r--r--silx/image/reconstruction.py25
-rw-r--r--silx/image/shapes.c9927
-rw-r--r--silx/image/sift.py26
-rw-r--r--silx/image/test/__init__.py2
-rw-r--r--silx/image/test/test_bilinear.py4
-rw-r--r--silx/image/test/test_shapes.py4
-rw-r--r--silx/image/test/test_tomography.py66
-rw-r--r--silx/image/tomography.py159
-rw-r--r--silx/io/__init__.py14
-rw-r--r--silx/io/commonh5.py991
-rw-r--r--silx/io/convert.py302
-rw-r--r--silx/io/dictdump.py158
-rw-r--r--silx/io/fabioh5.py569
-rw-r--r--silx/io/nxdata.py114
-rw-r--r--silx/io/rawh5.py71
-rw-r--r--silx/io/setup.py4
-rw-r--r--silx/io/specfile.c (renamed from silx/io/specfile/specfile.c)14347
-rw-r--r--silx/io/specfile.pyx (renamed from silx/io/specfile/specfile.pyx)191
-rw-r--r--silx/io/specfile/include/Lists.h31
-rw-r--r--silx/io/specfile/include/SpecFile.h32
-rw-r--r--silx/io/specfile/include/SpecFileP.h31
-rw-r--r--silx/io/specfile/include/locale_management.h27
-rw-r--r--silx/io/specfile/src/locale_management.c31
-rw-r--r--silx/io/specfile/src/sfdata.c31
-rw-r--r--silx/io/specfile/src/sfheader.c31
-rw-r--r--silx/io/specfile/src/sfindex.c31
-rw-r--r--silx/io/specfile/src/sfinit.c31
-rw-r--r--silx/io/specfile/src/sflabel.c31
-rw-r--r--silx/io/specfile/src/sflists.c31
-rw-r--r--silx/io/specfile/src/sfmca.c31
-rw-r--r--silx/io/specfile/src/sftools.c31
-rw-r--r--silx/io/specfile/src/sfwrite.c31
-rw-r--r--silx/io/specfile_wrapper.pxd (renamed from silx/io/specfile/specfile_wrapper.pxd)0
-rw-r--r--silx/io/spech5.py1573
-rw-r--r--silx/io/spectoh5.py301
-rw-r--r--silx/io/test/__init__.py6
-rw-r--r--silx/io/test/test_commonh5.py306
-rw-r--r--silx/io/test/test_fabioh5.py93
-rw-r--r--silx/io/test/test_nxdata.py7
-rw-r--r--silx/io/test/test_rawh5.py96
-rw-r--r--silx/io/test/test_specfile.py9
-rw-r--r--silx/io/test/test_specfilewrapper.py3
-rw-r--r--silx/io/test/test_spech5.py123
-rw-r--r--silx/io/test/test_spectoh5.py23
-rw-r--r--silx/io/test/test_utils.py186
-rw-r--r--silx/io/utils.py141
-rw-r--r--silx/math/chistogramnd.c (renamed from silx/math/histogramnd/chistogramnd.c)17088
-rw-r--r--silx/math/chistogramnd.pyx (renamed from silx/math/histogramnd/chistogramnd.pyx)28
-rw-r--r--silx/math/chistogramnd_lut.c (renamed from silx/math/histogramnd/chistogramnd_lut.c)26584
-rw-r--r--silx/math/chistogramnd_lut.pyx (renamed from silx/math/histogramnd/chistogramnd_lut.pyx)23
-rw-r--r--silx/math/combo.c (renamed from silx/math/combo/combo.c)23088
-rw-r--r--silx/math/combo.pyx (renamed from silx/math/combo/combo.pyx)106
-rw-r--r--silx/math/fit/filters.c (renamed from silx/math/fit/filters/filters.c)11128
-rw-r--r--silx/math/fit/filters.pyx (renamed from silx/math/fit/filters/filters.pyx)3
-rw-r--r--silx/math/fit/filters_wrapper.pxd (renamed from silx/math/fit/filters/filters_wrapper.pxd)0
-rw-r--r--silx/math/fit/fittheories.py3
-rw-r--r--silx/math/fit/functions.c (renamed from silx/math/fit/functions/functions.c)16233
-rw-r--r--silx/math/fit/functions.pyx (renamed from silx/math/fit/functions/functions.pyx)41
-rw-r--r--silx/math/fit/functions_wrapper.pxd (renamed from silx/math/fit/functions/functions_wrapper.pxd)0
-rw-r--r--silx/math/fit/leastsq.py3
-rw-r--r--silx/math/fit/peaks.c (renamed from silx/math/fit/peaks/peaks.c)9310
-rw-r--r--silx/math/fit/peaks.pyx (renamed from silx/math/fit/peaks/peaks.pyx)5
-rw-r--r--silx/math/fit/peaks_wrapper.pxd (renamed from silx/math/fit/peaks/peaks_wrapper.pxd)0
-rw-r--r--silx/math/fit/setup.py41
-rw-r--r--silx/math/fit/test/test_fit.py5
-rw-r--r--silx/math/histogram.py6
-rw-r--r--silx/math/histogramnd_c.pxd (renamed from silx/math/histogramnd/histogramnd_c.pxd)2
-rw-r--r--silx/math/include/isnan.h (renamed from silx/math/combo/isnan.h)12
-rw-r--r--silx/math/marchingcubes.cpp (renamed from silx/math/marchingcubes/marchingcubes.cpp)12019
-rw-r--r--silx/math/marchingcubes.pyx (renamed from silx/math/marchingcubes/marchingcubes.pyx)4
-rw-r--r--silx/math/mc.pxd (renamed from silx/math/marchingcubes/mc.pxd)0
-rw-r--r--silx/math/medianfilter/__init__.py2
-rw-r--r--silx/math/medianfilter/include/median_filter.hpp154
-rw-r--r--silx/math/medianfilter/median_filter.pxd3
-rw-r--r--silx/math/medianfilter/medianfilter.cpp13266
-rw-r--r--silx/math/medianfilter/medianfilter.pyx145
-rw-r--r--silx/math/medianfilter/test/test_medianfilter.py446
-rw-r--r--silx/math/setup.py33
-rw-r--r--silx/math/test/benchmark_combo.py4
-rw-r--r--silx/math/test/test_HistogramndLut_nominal.py15
-rw-r--r--silx/math/test/test_combo.py141
-rw-r--r--silx/math/test/test_histogramnd_error.py25
-rw-r--r--silx/math/test/test_histogramnd_nominal.py21
-rw-r--r--silx/opencl/__init__.py2
-rw-r--r--silx/opencl/backprojection.py488
-rw-r--r--silx/opencl/common.py29
-rw-r--r--silx/opencl/linalg.py218
-rw-r--r--silx/opencl/medfilt.py8
-rw-r--r--silx/opencl/processing.py61
-rw-r--r--silx/opencl/projection.py419
-rw-r--r--silx/opencl/reconstruction.py381
-rw-r--r--silx/opencl/setup.py9
-rw-r--r--silx/opencl/test/__init__.py21
-rw-r--r--silx/opencl/test/test_array_utils.py161
-rw-r--r--silx/opencl/test/test_backprojection.py165
-rw-r--r--silx/opencl/test/test_linalg.py215
-rw-r--r--silx/opencl/test/test_projection.py139
-rw-r--r--silx/opencl/utils.py71
-rw-r--r--silx/resources/__init__.py231
-rw-r--r--silx/resources/gui/colormaps/inferno.npybin0 -> 3152 bytes
-rw-r--r--silx/resources/gui/colormaps/magma.npybin0 -> 3152 bytes
-rw-r--r--silx/resources/gui/colormaps/plasma.npybin0 -> 3152 bytes
-rw-r--r--silx/resources/gui/colormaps/viridis.npybin0 -> 3152 bytes
-rw-r--r--silx/resources/gui/icons/3d-plane-normal-x.svg23
-rw-r--r--silx/resources/gui/icons/3d-plane-normal-y.svg23
-rw-r--r--silx/resources/gui/icons/3d-plane-normal-z.svg27
-rw-r--r--silx/resources/gui/icons/3d-plane-pan.pngbin0 -> 1428 bytes
-rw-r--r--silx/resources/gui/icons/3d-plane-pan.svg14
-rw-r--r--silx/resources/gui/icons/3d-plane.svg19
-rw-r--r--silx/resources/gui/icons/arrow-keys.svg2
-rw-r--r--silx/resources/gui/icons/axis.pngbin0 -> 1740 bytes
-rw-r--r--silx/resources/gui/icons/axis.svg2
-rw-r--r--silx/resources/gui/icons/camera.svg23
-rw-r--r--silx/resources/gui/icons/close.svg70
-rw-r--r--silx/resources/gui/icons/colorbar.pngbin0 -> 657 bytes
-rw-r--r--silx/resources/gui/icons/colorbar.svg3
-rw-r--r--silx/resources/gui/icons/crosshair.svg42
-rw-r--r--silx/resources/gui/icons/cube-back.svg23
-rw-r--r--silx/resources/gui/icons/cube-bottom.svg23
-rw-r--r--silx/resources/gui/icons/cube-front.svg23
-rw-r--r--silx/resources/gui/icons/cube-left.svg23
-rw-r--r--silx/resources/gui/icons/cube-right.svg23
-rw-r--r--silx/resources/gui/icons/cube-top.svg23
-rw-r--r--silx/resources/gui/icons/cube.svg25
-rw-r--r--silx/resources/gui/icons/document-open.svg128
-rw-r--r--silx/resources/gui/icons/document-print.svg14
-rw-r--r--silx/resources/gui/icons/document-save.svg38
-rw-r--r--silx/resources/gui/icons/draw-brush.svg8
-rw-r--r--silx/resources/gui/icons/draw-pencil.svg41
-rw-r--r--silx/resources/gui/icons/draw-rubber.svg45
-rw-r--r--silx/resources/gui/icons/edit-copy.svg77
-rw-r--r--silx/resources/gui/icons/first.svg37
-rw-r--r--silx/resources/gui/icons/folder.svg122
-rw-r--r--silx/resources/gui/icons/image-mask.svg27
-rw-r--r--silx/resources/gui/icons/image-select-box.svg123
-rw-r--r--silx/resources/gui/icons/image-select-brush.svg125
-rw-r--r--silx/resources/gui/icons/image-select-erase-rubber.svg8
-rw-r--r--silx/resources/gui/icons/image-select-erase.svg4
-rw-r--r--silx/resources/gui/icons/item-0dim.svg2
-rw-r--r--silx/resources/gui/icons/item-1dim.svg2
-rw-r--r--silx/resources/gui/icons/item-2dim.svg2
-rw-r--r--silx/resources/gui/icons/item-3dim.svg10
-rw-r--r--silx/resources/gui/icons/item-ndim.svg46
-rw-r--r--silx/resources/gui/icons/item-none.pngbin0 -> 637 bytes
-rw-r--r--silx/resources/gui/icons/item-none.svg5
-rw-r--r--silx/resources/gui/icons/item-object.svg22
-rw-r--r--silx/resources/gui/icons/last.svg37
-rw-r--r--silx/resources/gui/icons/math-amplitude.pngbin0 -> 526 bytes
-rw-r--r--silx/resources/gui/icons/math-amplitude.svg3
-rw-r--r--silx/resources/gui/icons/math-derive.svg2
-rw-r--r--silx/resources/gui/icons/math-fit.svg35
-rw-r--r--silx/resources/gui/icons/math-imaginary.pngbin0 -> 630 bytes
-rw-r--r--silx/resources/gui/icons/math-imaginary.svg3
-rw-r--r--silx/resources/gui/icons/math-normalize.svg46
-rw-r--r--silx/resources/gui/icons/math-peak-reset.svg57
-rw-r--r--silx/resources/gui/icons/math-peak-search.svg58
-rw-r--r--silx/resources/gui/icons/math-peak.svg41
-rw-r--r--silx/resources/gui/icons/math-phase-color-log.pngbin0 -> 2256 bytes
-rw-r--r--silx/resources/gui/icons/math-phase-color-log.svg3
-rw-r--r--silx/resources/gui/icons/math-phase-color.pngbin0 -> 2127 bytes
-rw-r--r--silx/resources/gui/icons/math-phase-color.svg3
-rw-r--r--silx/resources/gui/icons/math-phase.pngbin0 -> 515 bytes
-rw-r--r--silx/resources/gui/icons/math-phase.svg3
-rw-r--r--silx/resources/gui/icons/math-real.pngbin0 -> 749 bytes
-rw-r--r--silx/resources/gui/icons/math-real.svg3
-rw-r--r--silx/resources/gui/icons/math-sigma.svg93
-rw-r--r--silx/resources/gui/icons/math-smooth.svg29
-rw-r--r--silx/resources/gui/icons/math-substract.svg56
-rw-r--r--silx/resources/gui/icons/math-swap-sign.svg59
-rw-r--r--silx/resources/gui/icons/math-ymin-to-zero.svg42
-rw-r--r--silx/resources/gui/icons/median-filter.svg76
-rw-r--r--silx/resources/gui/icons/next.svg33
-rw-r--r--silx/resources/gui/icons/normal.svg35
-rw-r--r--silx/resources/gui/icons/pan.pngbin0 -> 526 bytes
-rw-r--r--silx/resources/gui/icons/pan.svg9
-rw-r--r--silx/resources/gui/icons/pixel-intensities.svg39
-rw-r--r--silx/resources/gui/icons/plot-roi-above.svg2
-rw-r--r--silx/resources/gui/icons/plot-roi-below.svg2
-rw-r--r--silx/resources/gui/icons/plot-roi-between.svg2
-rw-r--r--silx/resources/gui/icons/plot-roi-reset.svg69
-rw-r--r--silx/resources/gui/icons/plot-roi.svg54
-rw-r--r--silx/resources/gui/icons/plot-toggle-points.svg54
-rw-r--r--silx/resources/gui/icons/plot-widget.svg2
-rw-r--r--silx/resources/gui/icons/plot-window-image.svg2
-rw-r--r--silx/resources/gui/icons/plot-window.svg2
-rw-r--r--silx/resources/gui/icons/plot-xauto.svg41
-rw-r--r--silx/resources/gui/icons/plot-xlog.svg47
-rw-r--r--silx/resources/gui/icons/plot-yauto.svg36
-rw-r--r--silx/resources/gui/icons/plot-ydown.svg32
-rw-r--r--silx/resources/gui/icons/plot-ylog.svg42
-rw-r--r--silx/resources/gui/icons/plot-yup.svg41
-rw-r--r--silx/resources/gui/icons/previous.svg33
-rw-r--r--silx/resources/gui/icons/process-working/00.png (renamed from silx/resources/gui/icons/animated/process-working-00.png)bin778 -> 778 bytes
-rw-r--r--silx/resources/gui/icons/process-working/01.png (renamed from silx/resources/gui/icons/animated/process-working-01.png)bin789 -> 789 bytes
-rw-r--r--silx/resources/gui/icons/process-working/02.png (renamed from silx/resources/gui/icons/animated/process-working-02.png)bin785 -> 785 bytes
-rw-r--r--silx/resources/gui/icons/process-working/03.png (renamed from silx/resources/gui/icons/animated/process-working-03.png)bin785 -> 785 bytes
-rw-r--r--silx/resources/gui/icons/process-working/04.png (renamed from silx/resources/gui/icons/animated/process-working-04.png)bin766 -> 766 bytes
-rw-r--r--silx/resources/gui/icons/process-working/05.png (renamed from silx/resources/gui/icons/animated/process-working-05.png)bin777 -> 777 bytes
-rw-r--r--silx/resources/gui/icons/process-working/06.png (renamed from silx/resources/gui/icons/animated/process-working-06.png)bin784 -> 784 bytes
-rw-r--r--silx/resources/gui/icons/process-working/07.png (renamed from silx/resources/gui/icons/animated/process-working-07.png)bin783 -> 783 bytes
-rw-r--r--silx/resources/gui/icons/process-working/08.png (renamed from silx/resources/gui/icons/animated/process-working-08.png)bin762 -> 762 bytes
-rw-r--r--silx/resources/gui/icons/process-working/09.png (renamed from silx/resources/gui/icons/animated/process-working-09.png)bin781 -> 781 bytes
-rw-r--r--silx/resources/gui/icons/process-working/10.png (renamed from silx/resources/gui/icons/animated/process-working-10.png)bin771 -> 771 bytes
-rw-r--r--silx/resources/gui/icons/process-working/11.png (renamed from silx/resources/gui/icons/animated/process-working-11.png)bin768 -> 768 bytes
-rw-r--r--silx/resources/gui/icons/process-working/12.png (renamed from silx/resources/gui/icons/animated/process-working-12.png)bin759 -> 759 bytes
-rw-r--r--silx/resources/gui/icons/process-working/13.png (renamed from silx/resources/gui/icons/animated/process-working-13.png)bin767 -> 767 bytes
-rw-r--r--silx/resources/gui/icons/process-working/14.png (renamed from silx/resources/gui/icons/animated/process-working-14.png)bin778 -> 778 bytes
-rw-r--r--silx/resources/gui/icons/process-working/15.png (renamed from silx/resources/gui/icons/animated/process-working-15.png)bin760 -> 760 bytes
-rw-r--r--silx/resources/gui/icons/process-working/16.png (renamed from silx/resources/gui/icons/animated/process-working-16.png)bin754 -> 754 bytes
-rw-r--r--silx/resources/gui/icons/process-working/17.png (renamed from silx/resources/gui/icons/animated/process-working-17.png)bin782 -> 782 bytes
-rw-r--r--silx/resources/gui/icons/process-working/18.png (renamed from silx/resources/gui/icons/animated/process-working-18.png)bin775 -> 775 bytes
-rw-r--r--silx/resources/gui/icons/process-working/19.png (renamed from silx/resources/gui/icons/animated/process-working-19.png)bin764 -> 764 bytes
-rw-r--r--silx/resources/gui/icons/process-working/20.png (renamed from silx/resources/gui/icons/animated/process-working-20.png)bin764 -> 764 bytes
-rw-r--r--silx/resources/gui/icons/process-working/21.png (renamed from silx/resources/gui/icons/animated/process-working-21.png)bin772 -> 772 bytes
-rw-r--r--silx/resources/gui/icons/process-working/22.png (renamed from silx/resources/gui/icons/animated/process-working-22.png)bin769 -> 769 bytes
-rw-r--r--silx/resources/gui/icons/process-working/23.png (renamed from silx/resources/gui/icons/animated/process-working-23.png)bin773 -> 773 bytes
-rw-r--r--silx/resources/gui/icons/process-working/24.png (renamed from silx/resources/gui/icons/animated/process-working-24.png)bin757 -> 757 bytes
-rw-r--r--silx/resources/gui/icons/process-working/25.png (renamed from silx/resources/gui/icons/animated/process-working-25.png)bin759 -> 759 bytes
-rw-r--r--silx/resources/gui/icons/process-working/26.png (renamed from silx/resources/gui/icons/animated/process-working-26.png)bin774 -> 774 bytes
-rw-r--r--silx/resources/gui/icons/process-working/27.png (renamed from silx/resources/gui/icons/animated/process-working-27.png)bin766 -> 766 bytes
-rw-r--r--silx/resources/gui/icons/process-working/28.png (renamed from silx/resources/gui/icons/animated/process-working-28.png)bin760 -> 760 bytes
-rw-r--r--silx/resources/gui/icons/process-working/29.png (renamed from silx/resources/gui/icons/animated/process-working-29.png)bin777 -> 777 bytes
-rw-r--r--silx/resources/gui/icons/process-working/30.png (renamed from silx/resources/gui/icons/animated/process-working-30.png)bin775 -> 775 bytes
-rw-r--r--silx/resources/gui/icons/profile-clear.svg45
-rw-r--r--silx/resources/gui/icons/profile1D.svg23
-rw-r--r--silx/resources/gui/icons/profile2D.svg29
-rw-r--r--silx/resources/gui/icons/remove.svg64
-rw-r--r--silx/resources/gui/icons/rotate-3d.pngbin0 -> 760 bytes
-rw-r--r--silx/resources/gui/icons/rotate-3d.svg7
-rw-r--r--silx/resources/gui/icons/rudder.svg26
-rw-r--r--silx/resources/gui/icons/selected.svg41
-rw-r--r--silx/resources/gui/icons/shape-polygon.svg24
-rw-r--r--silx/resources/gui/icons/shape-rectangle.svg27
-rw-r--r--silx/resources/gui/icons/shape-square.svg27
-rw-r--r--silx/resources/gui/icons/shape-vertical.svg31
-rw-r--r--silx/resources/gui/icons/silx.svg2
-rw-r--r--silx/resources/gui/icons/sliders-off.svg120
-rw-r--r--silx/resources/gui/icons/sliders-on.svg108
-rw-r--r--silx/resources/gui/icons/spec.svg58
-rw-r--r--silx/resources/gui/icons/test-png.pngbin233 -> 0 bytes
-rw-r--r--silx/resources/gui/icons/test-svg.svg15
-rw-r--r--silx/resources/gui/icons/view-1d.svg21
-rw-r--r--silx/resources/gui/icons/view-2d-stack.svg17
-rw-r--r--silx/resources/gui/icons/view-2d.svg13
-rw-r--r--silx/resources/gui/icons/view-3d.svg21
-rw-r--r--silx/resources/gui/icons/view-fullscreen.svg72
-rw-r--r--silx/resources/gui/icons/view-hdf5.svg15
-rw-r--r--silx/resources/gui/icons/view-nexus.svg72
-rw-r--r--silx/resources/gui/icons/view-nofullscreen.svg97
-rw-r--r--silx/resources/gui/icons/view-raw.svg29
-rw-r--r--silx/resources/gui/icons/view-refresh.svg36
-rw-r--r--silx/resources/gui/icons/view-text.svg25
-rw-r--r--silx/resources/gui/icons/window-new.svg41
-rw-r--r--silx/resources/gui/icons/zoom-back.pngbin0 -> 1432 bytes
-rw-r--r--silx/resources/gui/icons/zoom-back.svg2
-rw-r--r--silx/resources/gui/icons/zoom-in.svg109
-rw-r--r--silx/resources/gui/icons/zoom-original.svg109
-rw-r--r--silx/resources/gui/icons/zoom-out.svg103
-rw-r--r--silx/resources/gui/icons/zoom.svg97
-rw-r--r--silx/resources/gui/logo/silx.pngbin0 -> 21257 bytes
-rw-r--r--silx/resources/gui/logo/silx.svg118
-rw-r--r--silx/resources/opencl/addition.cl12
-rw-r--r--silx/resources/opencl/array_utils.cl33
-rw-r--r--silx/resources/opencl/backproj.cl485
-rw-r--r--silx/resources/opencl/backproj_helper.cl68
-rw-r--r--silx/resources/opencl/linalg.cl89
-rw-r--r--silx/resources/opencl/proj.cl345
-rw-r--r--silx/sx/__init__.py4
-rw-r--r--silx/sx/_plot.py27
-rw-r--r--silx/test/__init__.py6
-rw-r--r--silx/test/test_resources.py212
-rw-r--r--silx/test/utils.py10
-rw-r--r--silx/third_party/_local/enum.py877
-rw-r--r--silx/third_party/enum.py51
-rw-r--r--silx/utils/decorators.py71
-rw-r--r--silx/utils/deprecation.py117
-rw-r--r--silx/utils/proxy.py204
-rw-r--r--silx/utils/test/__init__.py22
-rw-r--r--silx/utils/test/test_deprecation.py107
-rw-r--r--silx/utils/test/test_proxy.py295
438 files changed, 129883 insertions, 75418 deletions
diff --git a/silx/__init__.py b/silx/__init__.py
index cfb306b..8dab7e1 100644
--- a/silx/__init__.py
+++ b/silx/__init__.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2016 European Synchrotron Radiation Facility
+# Copyright (c) 2015-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
@@ -32,7 +32,10 @@ __date__ = "23/05/2016"
import os as _os
import logging as _logging
-_logging.basicConfig() # Make sure logging is initialised
+
+# Attach a do nothing logging handler for silx
+_logging.getLogger(__name__).addHandler(_logging.NullHandler())
+
project = _os.path.basename(_os.path.dirname(_os.path.abspath(__file__)))
diff --git a/silx/__main__.py b/silx/__main__.py
index 4c05ef3..8323b03 100644
--- a/silx/__main__.py
+++ b/silx/__main__.py
@@ -2,7 +2,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2016 European Synchrotron Radiation Facility
+# Copyright (c) 2017 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -30,9 +30,9 @@ Your environment should provide a command `silx`. You can reach help with
`silx --help`, and check the version with `silx --version`.
"""
-__authors__ = ["V. Valls"]
+__authors__ = ["V. Valls", "P. Knobel"]
__license__ = "MIT"
-__date__ = "18/04/2017"
+__date__ = "29/06/2017"
import logging
@@ -46,8 +46,8 @@ import silx._version
def main():
"""Main function of the launcher
- This function is referenced in the setup.py file.
- Thisfor it is executed by a launcher script generated by setuptools.
+ This function is referenced in the setup.py file, to create a
+ launcher script generated by setuptools.
:rtype: int
:returns: The execution status
@@ -56,6 +56,12 @@ def main():
launcher.add_command("view",
module_name="silx.app.view",
description="Browse a data file with a GUI")
+ launcher.add_command("convert",
+ module_name="silx.app.convert",
+ description="Convert and concatenate files into a HDF5 file")
+ launcher.add_command("test",
+ module_name="silx.app.test_",
+ description="Launch silx unittest")
status = launcher.execute(sys.argv)
return status
diff --git a/silx/app/convert.py b/silx/app/convert.py
new file mode 100644
index 0000000..a092ec1
--- /dev/null
+++ b/silx/app/convert.py
@@ -0,0 +1,283 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2017 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""Convert silx supported data files into HDF5 files"""
+
+import ast
+import sys
+import os
+import argparse
+from glob import glob
+import logging
+import numpy
+import silx
+
+
+__authors__ = ["P. Knobel"]
+__license__ = "MIT"
+__date__ = "12/09/2017"
+
+
+_logger = logging.getLogger(__name__)
+"""Module logger"""
+
+
+def main(argv):
+ """
+ Main function to launch the converter as an application
+
+ :param argv: Command line arguments
+ :returns: exit status
+ """
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ 'input_files',
+ nargs="+",
+ help='Input files (EDF, SPEC)')
+ parser.add_argument(
+ '-o', '--output-uri',
+ nargs="?",
+ help='Output file (HDF5). If omitted, it will be the '
+ 'concatenated input file names, with a ".h5" suffix added.'
+ ' An URI can be provided to write the data into a specific '
+ 'group in the output file: /path/to/file::/path/to/group')
+ parser.add_argument(
+ '-m', '--mode',
+ default="w-",
+ help='Write mode: "r+" (read/write, file must exist), '
+ '"w" (write, existing file is lost), '
+ '"w-" (write, fail if file exists) or '
+ '"a" (read/write if exists, create otherwise)')
+ parser.add_argument(
+ '--no-root-group',
+ action="store_true",
+ help='This option disables the default behavior of creating a '
+ 'root group (entry) for each file to be converted. When '
+ 'merging multiple input files, this can cause conflicts '
+ 'when datasets have the same name (see --overwrite-data).')
+ parser.add_argument(
+ '--overwrite-data',
+ action="store_true",
+ help='If the output path exists and an input dataset has the same'
+ ' name as an existing output dataset, overwrite the output '
+ 'dataset (in modes "r+" or "a").')
+ parser.add_argument(
+ '--min-size',
+ type=int,
+ default=500,
+ help='Minimum number of elements required to be in a dataset to '
+ 'apply compression or chunking (default 500).')
+ parser.add_argument(
+ '--chunks',
+ nargs="?",
+ const="auto",
+ help='Chunk shape. Provide an argument that evaluates as a python '
+ 'tuple (e.g. "(1024, 768)"). If this option is provided without '
+ 'specifying an argument, the h5py library will guess a chunk for '
+ 'you. Note that if you specify an explicit chunking shape, it '
+ 'will be applied identically to all datasets with a large enough '
+ 'size (see --min-size). ')
+ parser.add_argument(
+ '--compression',
+ nargs="?",
+ const="gzip",
+ help='Compression filter. By default, the datasets in the output '
+ 'file are not compressed. If this option is specified without '
+ 'argument, the GZIP compression is used. Additional compression '
+ 'filters may be available, depending on your HDF5 installation.')
+
+ def check_gzip_compression_opts(value):
+ ivalue = int(value)
+ if ivalue < 0 or ivalue > 9:
+ raise argparse.ArgumentTypeError(
+ "--compression-opts must be an int from 0 to 9")
+ return ivalue
+
+ parser.add_argument(
+ '--compression-opts',
+ type=check_gzip_compression_opts,
+ help='Compression options. For "gzip", this may be an integer from '
+ '0 to 9, with a default of 4. This is only supported for GZIP.')
+ parser.add_argument(
+ '--shuffle',
+ action="store_true",
+ help='Enables the byte shuffle filter, may improve the compression '
+ 'ratio for block oriented compressors like GZIP or LZF.')
+ parser.add_argument(
+ '--fletcher32',
+ action="store_true",
+ help='Adds a checksum to each chunk to detect data corruption.')
+ parser.add_argument(
+ '--debug',
+ action="store_true",
+ default=False,
+ help='Set logging system in debug mode')
+
+ options = parser.parse_args(argv[1:])
+
+ # some shells (windows) don't interpret wildcard characters (*, ?, [])
+ old_input_list = list(options.input_files)
+ options.input_files = []
+ for fname in old_input_list:
+ globbed_files = glob(fname)
+ if not globbed_files:
+ # no files found, keep the name as it is, to raise an error later
+ options.input_files += [fname]
+ else:
+ options.input_files += globbed_files
+ old_input_list = None
+
+ if options.debug:
+ logging.root.setLevel(logging.DEBUG)
+
+ # Import most of the things here to be sure to use the right logging level
+ try:
+ # it should be loaded before h5py
+ import hdf5plugin # noqa
+ except ImportError:
+ _logger.debug("Backtrace", exc_info=True)
+ hdf5plugin = None
+
+ 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\"."
+ _logger.debug(message)
+
+ # Test that the output path is writeable
+ if options.output_uri is None:
+ input_basenames = [os.path.basename(name) for name in options.input_files]
+ output_name = ''.join(input_basenames) + ".h5"
+ _logger.info("No output file specified, using %s", output_name)
+ hdf5_path = "/"
+ else:
+ if "::" in options.output_uri:
+ output_name, hdf5_path = options.output_uri.split("::")
+ else:
+ output_name, hdf5_path = options.output_uri, "/"
+
+ if os.path.isfile(output_name):
+ if options.mode == "w-":
+ _logger.error("Output file %s exists and mode is 'w-'"
+ " (write, file must not exist). Aborting.",
+ output_name)
+ return -1
+ elif not os.access(output_name, os.W_OK):
+ _logger.error("Output file %s exists and is not writeable.",
+ output_name)
+ return -1
+ elif options.mode == "w":
+ _logger.info("Output file %s exists and mode is 'w'. "
+ "Overwriting existing file.", output_name)
+ elif options.mode in ["a", "r+"]:
+ _logger.info("Appending data to existing file %s.",
+ output_name)
+ else:
+ if options.mode == "r+":
+ _logger.error("Output file %s does not exist and mode is 'r+'"
+ " (append, file must exist). Aborting.",
+ output_name)
+ return -1
+ else:
+ _logger.info("Creating new output file %s.",
+ output_name)
+
+ # Test that all input files exist and are readable
+ bad_input = False
+ for fname in options.input_files:
+ if not os.access(fname, os.R_OK):
+ _logger.error("Cannot read input file %s.",
+ fname)
+ bad_input = True
+ if bad_input:
+ _logger.error("Aborting.")
+ return -1
+
+ # create_dataset special args
+ create_dataset_args = {}
+ if options.chunks is not None:
+ if options.chunks.lower() in ["auto", "true"]:
+ create_dataset_args["chunks"] = True
+ else:
+ try:
+ chunks = ast.literal_eval(options.chunks)
+ except (ValueError, SyntaxError):
+ _logger.error("Invalid --chunks argument %s", options.chunks)
+ return -1
+ if not isinstance(chunks, (tuple, list)):
+ _logger.error("--chunks argument str does not evaluate to a tuple")
+ return -1
+ else:
+ nitems = numpy.prod(chunks)
+ nbytes = nitems * 8
+ if nbytes > 10**6:
+ _logger.warning("Requested chunk size might be larger than"
+ " the default 1MB chunk cache, for float64"
+ " data. This can dramatically affect I/O "
+ "performances.")
+ create_dataset_args["chunks"] = chunks
+
+ if options.compression is not None:
+ create_dataset_args["compression"] = options.compression
+
+ if options.compression_opts is not None:
+ create_dataset_args["compression_opts"] = options.compression_opts
+
+ if options.shuffle:
+ create_dataset_args["shuffle"] = True
+
+ if options.fletcher32:
+ create_dataset_args["fletcher32"] = True
+
+ with h5py.File(output_name, mode=options.mode) as h5f:
+ for input_name in options.input_files:
+ hdf5_path_for_file = hdf5_path
+ if not options.no_root_group:
+ hdf5_path_for_file = hdf5_path.rstrip("/") + "/" + os.path.basename(input_name)
+ write_to_h5(input_name, h5f,
+ h5path=hdf5_path_for_file,
+ overwrite_data=options.overwrite_data,
+ create_dataset_args=create_dataset_args,
+ min_size=options.min_size)
+
+ # append the convert command to the creator attribute, for NeXus files
+ creator = h5f[hdf5_path_for_file].attrs.get("creator", b"").decode()
+ convert_command = " ".join(argv)
+ if convert_command not in creator:
+ h5f[hdf5_path_for_file].attrs["creator"] = \
+ numpy.string_(creator + "; convert command: %s" % " ".join(argv))
+
+ return 0
diff --git a/silx/app/qtutils.py b/silx/app/qtutils.py
new file mode 100644
index 0000000..4c29c84
--- /dev/null
+++ b/silx/app/qtutils.py
@@ -0,0 +1,243 @@
+# 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.
+#
+# ############################################################################*/
+"""Qt utils for Silx applications"""
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "22/09/2017"
+
+import sys
+
+try:
+ # it should be loaded before h5py
+ import hdf5plugin # noqa
+except ImportError:
+ hdf5plugin = None
+
+try:
+ import h5py
+except ImportError:
+ h5py = None
+
+try:
+ import fabio
+except ImportError:
+ fabio = None
+
+from silx.gui import qt
+from silx.gui import icons
+
+_LICENSE_TEMPLATE = """<p align="center">
+<b>Copyright (C) {year} European Synchrotron Radiation Facility</b>
+</p>
+
+<p align="justify">
+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:
+</p>
+
+<p align="justify">
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+</p>
+
+<p align="justify">
+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.
+</p>
+"""
+
+
+class About(qt.QDialog):
+ """
+ Util dialog to display an common about box for all the silx GUIs.
+ """
+
+ def __init__(self, parent=None):
+ """
+ :param files_: List of HDF5 or Spec files (pathes or
+ :class:`silx.io.spech5.SpecH5` or :class:`h5py.File`
+ instances)
+ """
+ super(About, self).__init__(parent)
+ self.__createLayout()
+ self.setSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed)
+ self.setModal(True)
+ self.setApplicationName(None)
+
+ def __createLayout(self):
+ layout = qt.QVBoxLayout(self)
+ layout.setContentsMargins(24, 15, 24, 20)
+ layout.setSpacing(8)
+
+ self.__label = qt.QLabel(self)
+ self.__label.setWordWrap(True)
+ flags = self.__label.textInteractionFlags()
+ flags = flags | qt.Qt.TextSelectableByKeyboard
+ flags = flags | qt.Qt.TextSelectableByMouse
+ self.__label.setTextInteractionFlags(flags)
+ self.__label.setOpenExternalLinks(True)
+ self.__label.setSizePolicy(qt.QSizePolicy.Minimum, qt.QSizePolicy.Preferred)
+
+ licenseButton = qt.QPushButton(self)
+ licenseButton.setText("License...")
+ licenseButton.clicked.connect(self.__displayLicense)
+ licenseButton.setAutoDefault(False)
+
+ self.__options = qt.QDialogButtonBox()
+ self.__options.addButton(licenseButton, qt.QDialogButtonBox.ActionRole)
+ okButton = self.__options.addButton(qt.QDialogButtonBox.Ok)
+ okButton.setDefault(True)
+ okButton.clicked.connect(self.accept)
+
+ layout.addWidget(self.__label)
+ layout.addWidget(self.__options)
+ layout.setStretch(0, 100)
+ layout.setStretch(1, 0)
+
+ def getHtmlLicense(self):
+ """Returns the text license in HTML format.
+
+ :rtype: str
+ """
+ from silx._version import __date__ as date
+ year = date.split("/")[2]
+ info = dict(
+ year=year
+ )
+ textLicense = _LICENSE_TEMPLATE.format(**info)
+ return textLicense
+
+ def __displayLicense(self):
+ """Displays the license used by silx."""
+ text = self.getHtmlLicense()
+ licenseDialog = qt.QMessageBox(self)
+ licenseDialog.setWindowTitle("License")
+ licenseDialog.setText(text)
+ licenseDialog.exec_()
+
+ def setApplicationName(self, name):
+ self.__applicationName = name
+ if name is None:
+ self.setWindowTitle("About")
+ else:
+ self.setWindowTitle("About %s" % name)
+ self.__updateText()
+
+ @staticmethod
+ def __formatOptionalLibraries(name, isAvailable):
+ """Utils to format availability of features"""
+ if isAvailable:
+ template = '<b>%s</b> is <font color="green">installed</font>'
+ else:
+ template = '<b>%s</b> is <font color="red">not installed</font>'
+ return template % name
+
+ def __updateText(self):
+ """Update the content of the dialog according to the settings."""
+ import silx._version
+
+ message = """<table>
+ <tr><td width="50%" align="center" valign="middle">
+ <img src="{silx_image_path}" width="100" />
+ </td><td width="50%" align="center" valign="middle">
+ <b>{application_name}</b>
+ <br />
+ <br />{silx_version}
+ <br />
+ <br /><a href="{project_url}">Upstream project on GitHub</a>
+ </td></tr>
+ </table>
+ <dl>
+ <dt><b>Silx version</b></dt><dd>{silx_version}</dd>
+ <dt><b>Qt version</b></dt><dd>{qt_version}</dd>
+ <dt><b>Qt binding</b></dt><dd>{qt_binding}</dd>
+ <dt><b>Python version</b></dt><dd>{python_version}</dd>
+ <dt><b>Optional libraries</b></dt><dd>{optional_lib}</dd>
+ </dl>
+ <p>
+ Copyright (C) <a href="{esrf_url}">European Synchrotron Radiation Facility</a>
+ </p>
+ """
+ optional_lib = []
+ optional_lib.append(self.__formatOptionalLibraries("FabIO", fabio is not None))
+ optional_lib.append(self.__formatOptionalLibraries("H5py", h5py is not None))
+ optional_lib.append(self.__formatOptionalLibraries("hdf5plugin", hdf5plugin is not None))
+
+ # Access to the logo in SVG or PNG
+ logo = icons.getQFile("../logo/silx")
+
+ info = dict(
+ application_name=self.__applicationName,
+ esrf_url="http://www.esrf.eu",
+ project_url="https://github.com/silx-kit/silx",
+ silx_version=silx._version.version,
+ qt_binding=qt.BINDING,
+ qt_version=qt.qVersion(),
+ python_version=sys.version.replace("\n", "<br />"),
+ optional_lib="<br />".join(optional_lib),
+ silx_image_path=logo.fileName()
+ )
+
+ self.__label.setText(message.format(**info))
+ self.__updateSize()
+
+ def __updateSize(self):
+ """Force the size to a QMessageBox like size."""
+ screenSize = qt.QApplication.desktop().availableGeometry(qt.QCursor.pos()).size()
+ hardLimit = min(screenSize.width() - 480, 1000)
+ if screenSize.width() <= 1024:
+ hardLimit = screenSize.width()
+ softLimit = min(screenSize.width() / 2, 420)
+
+ layoutMinimumSize = self.layout().totalMinimumSize()
+ width = layoutMinimumSize.width()
+ if width > softLimit:
+ width = softLimit
+ if width > hardLimit:
+ width = hardLimit
+
+ height = layoutMinimumSize.height()
+ self.setFixedSize(width, height)
+
+ @staticmethod
+ def about(parent, applicationName):
+ """Displays a silx about box with title and text text.
+
+ :param qt.QWidget parent: The parent widget
+ :param str title: The title of the dialog
+ :param str applicationName: The content of the dialog
+ """
+ dialog = About(parent)
+ dialog.setApplicationName(applicationName)
+ dialog.exec_()
diff --git a/silx/app/test/__init__.py b/silx/app/test/__init__.py
index 54241dc..0c22386 100644
--- a/silx/app/test/__init__.py
+++ b/silx/app/test/__init__.py
@@ -26,16 +26,14 @@ __authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "30/03/2017"
-
-import logging
-import os
-import sys
import unittest
-
-_logger = logging.getLogger(__name__)
+from . import test_view
+from . import test_convert
def suite():
test_suite = unittest.TestSuite()
+ test_suite.addTest(test_view.suite())
+ test_suite.addTest(test_convert.suite())
return test_suite
diff --git a/silx/app/test/test_convert.py b/silx/app/test/test_convert.py
new file mode 100644
index 0000000..3215460
--- /dev/null
+++ b/silx/app/test/test_convert.py
@@ -0,0 +1,182 @@
+# 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.
+#
+# ###########################################################################*/
+"""Module testing silx.app.convert"""
+
+__authors__ = ["P. Knobel"]
+__license__ = "MIT"
+__date__ = "12/09/2017"
+
+
+import os
+import sys
+import tempfile
+import unittest
+import io
+import gc
+
+try:
+ import h5py
+except ImportError:
+ h5py = None
+
+import silx
+from .. import convert
+from silx.test import utils
+
+
+
+# content of a spec file
+sftext = """#F /tmp/sf.dat
+#E 1455180875
+#D Thu Feb 11 09:54:35 2016
+#C imaging User = opid17
+#O0 Pslit HGap MRTSlit UP MRTSlit DOWN
+#O1 Sslit1 VOff Sslit1 HOff Sslit1 VGap
+#o0 pshg mrtu mrtd
+#o2 ss1vo ss1ho ss1vg
+
+#J0 Seconds IA ion.mono Current
+#J1 xbpmc2 idgap1 Inorm
+
+#S 1 ascan ss1vo -4.55687 -0.556875 40 0.2
+#D Thu Feb 11 09:55:20 2016
+#T 0.2 (Seconds)
+#P0 180.005 -0.66875 0.87125
+#P1 14.74255 16.197579 12.238283
+#N 4
+#L MRTSlit UP second column 3rd_col
+-1.23 5.89 8
+8.478100E+01 5 1.56
+3.14 2.73 -3.14
+1.2 2.3 3.4
+
+#S 1 aaaaaa
+#D Thu Feb 11 10:00:32 2016
+#@MCADEV 1
+#@MCA %16C
+#@CHANN 3 0 2 1
+#@CALIB 1 2 3
+#N 3
+#L uno duo
+1 2
+@A 0 1 2
+@A 10 9 8
+3 4
+@A 3.1 4 5
+@A 7 6 5
+5 6
+@A 6 7.7 8
+@A 4 3 2
+"""
+
+
+class TestConvertCommand(unittest.TestCase):
+ """Test command line parsing"""
+
+ def testHelp(self):
+ # option -h must cause a `raise SystemExit` or a `return 0`
+ try:
+ result = convert.main(["convert", "--help"])
+ except SystemExit as e:
+ result = e.args[0]
+ self.assertEqual(result, 0)
+
+ @unittest.skipUnless(h5py is None,
+ "h5py is installed, this test is specific to h5py missing")
+ @utils.test_logging(convert._logger.name, error=1)
+ def testH5pyNotInstalled(self):
+ 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
+ try:
+ result = convert.main(["convert", "--foo"])
+ except SystemExit as e:
+ result = e.args[0]
+ self.assertNotEqual(result, 0)
+
+ @unittest.skipIf(h5py is None, "h5py is required to test convert")
+ @utils.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()
+
+ # write a temporary SPEC file
+ specname = os.path.join(tempdir, "input.dat")
+ with io.open(specname, "wb") as fd:
+ if sys.version < '3.0':
+ fd.write(sftext)
+ else:
+ fd.write(bytes(sftext, 'ascii'))
+
+ # convert it
+ h5name = os.path.join(tempdir, "output.h5")
+ command_list = ["convert", "-m", "w",
+ "--no-root-group", specname, "-o", h5name]
+ result = convert.main(command_list)
+
+ self.assertEqual(result, 0)
+ self.assertTrue(os.path.isfile(h5name))
+
+ with h5py.File(h5name, "r") as h5f:
+ title12 = h5f["/1.2/title"][()]
+ if sys.version > '3.0':
+ title12 = title12.decode()
+ self.assertEqual(title12,
+ "1 aaaaaa")
+
+ creator = h5f.attrs.get("creator")
+ self.assertIsNotNone(creator, "No creator attribute in NXroot group")
+ creator = creator.decode() # make sure we can compare creator with native string
+ self.assertTrue(creator.startswith("silx %s" % silx.version))
+ command = " ".join(command_list)
+ self.assertTrue(creator.endswith(command))
+
+ # delete input file
+ gc.collect() # necessary to free spec file on Windows
+ os.unlink(specname)
+ os.unlink(h5name)
+ os.rmdir(tempdir)
+
+
+def suite():
+ test_suite = unittest.TestSuite()
+ loader = unittest.defaultTestLoader.loadTestsFromTestCase
+ test_suite.addTest(loader(TestConvertCommand))
+ return test_suite
+
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')
diff --git a/silx/app/test/test_view.py b/silx/app/test/test_view.py
index 774bc01..e55e4f3 100644
--- a/silx/app/test/test_view.py
+++ b/silx/app/test/test_view.py
@@ -26,13 +26,29 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "12/04/2017"
+__date__ = "29/09/2017"
import unittest
-from silx.gui.test.utils import TestCaseQt
-from .. import view
import sys
+import os
+
+
+# TODO: factor this code with silx.gui.test
+with_qt = False
+if sys.platform.startswith('linux') and not os.environ.get('DISPLAY', ''):
+ reason = 'test disabled (DISPLAY env. variable not set)'
+ view = None
+ TestCaseQt = unittest.TestCase
+elif os.environ.get('WITH_QT_TEST', 'True') == 'False':
+ reason = "test disabled (env. variable WITH_QT_TEST=False)"
+ view = None
+ TestCaseQt = unittest.TestCase
+else:
+ from silx.gui.test.utils import TestCaseQt
+ from .. import view
+ with_qt = True
+ reason = ""
class QApplicationMock(object):
@@ -64,6 +80,7 @@ class ViewerMock(object):
pass
+@unittest.skipUnless(with_qt, "Qt binding required for TestLauncher")
class TestLauncher(unittest.TestCase):
"""Test command line parsing"""
@@ -83,9 +100,9 @@ class TestLauncher(unittest.TestCase):
super(TestLauncher, cls).tearDownClass()
def testHelp(self):
+ # option -h must cause a raise SystemExit or a return 0
try:
result = view.main(["view", "--help"])
- self.assertNotEqual(result, 0)
except SystemExit as e:
result = e.args[0]
self.assertEqual(result, 0)
@@ -93,7 +110,6 @@ class TestLauncher(unittest.TestCase):
def testWrongOption(self):
try:
result = view.main(["view", "--foo"])
- self.assertNotEqual(result, 0)
except SystemExit as e:
result = e.args[0]
self.assertNotEqual(result, 0)
@@ -101,10 +117,9 @@ class TestLauncher(unittest.TestCase):
def testWrongFile(self):
try:
result = view.main(["view", "__file.not.found__"])
- self.assertNotEqual(result, 0)
except SystemExit as e:
result = e.args[0]
- self.assertNotEqual(result, 0)
+ self.assertEqual(result, 0)
def testFile(self):
# sys.executable is an existing readable file
@@ -118,8 +133,10 @@ class TestLauncher(unittest.TestCase):
class TestViewer(TestCaseQt):
"""Test for Viewer class"""
+ @unittest.skipUnless(with_qt, reason)
def testConstruct(self):
- widget = view.Viewer()
+ if view is not None:
+ widget = view.Viewer()
self.qWaitForWindowExposed(widget)
diff --git a/silx/app/test_.py b/silx/app/test_.py
new file mode 100644
index 0000000..7f95085
--- /dev/null
+++ b/silx/app/test_.py
@@ -0,0 +1,175 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""Launch unittests of the library"""
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "04/08/2017"
+
+import sys
+import os
+import argparse
+import logging
+import unittest
+
+
+class StreamHandlerUnittestReady(logging.StreamHandler):
+ """The unittest class TestResult redefine sys.stdout/err to capture
+ stdout/err from tests and to display them only when a test fail.
+
+ This class allow to use unittest stdout-capture by using the last sys.stdout
+ and not a cached one.
+ """
+
+ def emit(self, record):
+ """
+ :type record: logging.LogRecord
+ """
+ self.stream = sys.stderr
+ super(StreamHandlerUnittestReady, self).emit(record)
+
+ def flush(self):
+ pass
+
+
+def createBasicHandler():
+ """Create the handler using the basic configuration"""
+ hdlr = StreamHandlerUnittestReady()
+ fs = logging.BASIC_FORMAT
+ dfs = None
+ fmt = logging.Formatter(fs, dfs)
+ hdlr.setFormatter(fmt)
+ return hdlr
+
+
+# Use an handler compatible with unittests, else use_buffer is not working
+for h in logging.root.handlers:
+ logging.root.removeHandler(h)
+logging.root.addHandler(createBasicHandler())
+logging.captureWarnings(True)
+
+_logger = logging.getLogger(__name__)
+"""Module logger"""
+
+
+class TextTestResultWithSkipList(unittest.TextTestResult):
+ """Override default TextTestResult to display list of skipped tests at the
+ end
+ """
+
+ def printErrors(self):
+ unittest.TextTestResult.printErrors(self)
+ # Print skipped tests at the end
+ self.printErrorList("SKIPPED", self.skipped)
+
+
+def main(argv):
+ """
+ Main function to launch the unittests as an application
+
+ :param argv: Command line arguments
+ :returns: exit status
+ """
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("-v", "--verbose", default=0,
+ action="count", dest="verbose",
+ help="Increase verbosity. Option -v prints additional " +
+ "INFO messages. Use -vv for full verbosity, " +
+ "including debug messages and test help strings.")
+ parser.add_argument("-x", "--no-gui", dest="gui", default=True,
+ action="store_false",
+ help="Disable the test of the graphical use interface")
+ parser.add_argument("-g", "--no-opengl", dest="opengl", default=True,
+ action="store_false",
+ help="Disable tests using OpenGL")
+ parser.add_argument("-o", "--no-opencl", dest="opencl", default=True,
+ action="store_false",
+ help="Disable the test of the OpenCL part")
+ parser.add_argument("-l", "--low-mem", dest="low_mem", default=False,
+ action="store_true",
+ help="Disable test with large memory consumption (>100Mbyte")
+ parser.add_argument("--qt-binding", dest="qt_binding", default=None,
+ help="Force using a Qt binding, from 'PyQt4', 'PyQt5', or 'PySide'")
+
+ options = parser.parse_args(argv[1:])
+
+ test_verbosity = 1
+ use_buffer = True
+ if options.verbose == 1:
+ logging.root.setLevel(logging.INFO)
+ _logger.info("Set log level: INFO")
+ test_verbosity = 2
+ use_buffer = False
+ elif options.verbose > 1:
+ logging.root.setLevel(logging.DEBUG)
+ _logger.info("Set log level: DEBUG")
+ test_verbosity = 2
+ use_buffer = False
+
+ if not options.gui:
+ os.environ["WITH_QT_TEST"] = "False"
+
+ if not options.opencl:
+ os.environ["SILX_OPENCL"] = "False"
+
+ if not options.opengl:
+ os.environ["WITH_GL_TEST"] = "False"
+
+ if options.low_mem:
+ os.environ["SILX_TEST_LOW_MEM"] = "True"
+
+ if options.qt_binding:
+ binding = options.qt_binding.lower()
+ if binding == "pyqt4":
+ _logger.info("Force using PyQt4")
+ import PyQt4.QtCore # noqa
+ elif binding == "pyqt5":
+ _logger.info("Force using PyQt5")
+ import PyQt5.QtCore # noqa
+ elif binding == "pyside":
+ _logger.info("Force using PySide")
+ import PySide.QtCore # noqa
+ else:
+ raise ValueError("Qt binding '%s' is unknown" % options.qt_binding)
+
+ # Run the tests
+ runnerArgs = {}
+ runnerArgs["verbosity"] = test_verbosity
+ runnerArgs["buffer"] = use_buffer
+ runner = unittest.TextTestRunner(**runnerArgs)
+ runner.resultclass = TextTestResultWithSkipList
+
+ # Display the result when using CTRL-C
+ unittest.installHandler()
+
+ import silx.test
+ test_suite = unittest.TestSuite()
+ test_suite.addTest(silx.test.suite())
+ result = runner.run(test_suite)
+
+ if result.wasSuccessful():
+ exit_status = 0
+ else:
+ exit_status = 1
+ return exit_status
diff --git a/silx/app/view.py b/silx/app/view.py
index 8fdabde..e8507f4 100644
--- a/silx/app/view.py
+++ b/silx/app/view.py
@@ -1,6 +1,6 @@
# coding: utf-8
# /*##########################################################################
-# Copyright (C) 2016 European Synchrotron Radiation Facility
+# 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
@@ -25,7 +25,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "12/04/2017"
+__date__ = "02/10/2017"
import sys
import os
@@ -33,30 +33,10 @@ import argparse
import logging
import collections
-
-logging.basicConfig()
_logger = logging.getLogger(__name__)
"""Module logger"""
-try:
- # it should be loaded before h5py
- import hdf5plugin # noqa
-except ImportError:
- hdf5plugin = None
-
-try:
- import h5py
- import silx.gui.hdf5
-except ImportError:
- h5py = None
-
-try:
- import fabio
-except ImportError:
- fabio = None
-
from silx.gui import qt
-from silx.gui.data.DataViewerFrame import DataViewerFrame
class Viewer(qt.QMainWindow):
@@ -71,6 +51,10 @@ class Viewer(qt.QMainWindow):
:class:`silx.io.spech5.SpecH5` or :class:`h5py.File`
instances)
"""
+ # Import it here to be sure to use the right logging level
+ import silx.gui.hdf5
+ from silx.gui.data.DataViewerFrame import DataViewerFrame
+
qt.QMainWindow.__init__(self)
self.setWindowTitle("Silx viewer")
@@ -97,14 +81,15 @@ class Viewer(qt.QMainWindow):
self.setCentralWidget(main_panel)
- self.__treeview.selectionModel().selectionChanged.connect(self.displayData)
+ model = self.__treeview.selectionModel()
+ model.selectionChanged.connect(self.displayData)
+ self.__treeview.addContextMenuCallback(self.closeAndSyncCustomContextMenu)
- self.__treeview.addContextMenuCallback(self.customContextMenu)
- # lambda function will never be called cause we store it as weakref
- self.__treeview.addContextMenuCallback(lambda event: None)
- # you have to store it first
- self.__store_lambda = lambda event: self.closeAndSyncCustomContextMenu(event)
- self.__treeview.addContextMenuCallback(self.__store_lambda)
+ treeModel = self.__treeview.findHdf5TreeModel()
+ columns = list(treeModel.COLUMN_IDS)
+ columns.remove(treeModel.DESCRIPTION_COLUMN)
+ columns.remove(treeModel.NODE_COLUMN)
+ self.__treeview.header().setSections(columns)
self.createActions()
self.createMenus()
@@ -159,15 +144,17 @@ class Viewer(qt.QMainWindow):
extensions = collections.OrderedDict()
# expect h5py
- extensions["HDF5 files"] = "*.h5"
+ extensions["HDF5 files"] = "*.h5 *.hdf"
+ extensions["NeXus files"] = "*.nx *.nxs *.h5 *.hdf"
# no dependancy
- extensions["Spec files"] = "*.dat *.spec *.mca"
+ extensions["NeXus layout from spec files"] = "*.dat *.spec *.mca"
+ extensions["Numpy binary files"] = "*.npz *.npy"
# expect fabio
- extensions["EDF files"] = "*.edf"
- extensions["TIFF image files"] = "*.tif *.tiff"
- extensions["NumPy binary files"] = "*.npy"
- extensions["CBF files"] = "*.cbf"
- extensions["MarCCD image files"] = "*.mccd"
+ extensions["NeXus layout from raster images"] = "*.edf *.tif *.tiff *.cbf *.mccd"
+ 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"
filters = []
filters.append("All supported files (%s)" % " ".join(extensions.values()))
@@ -180,48 +167,8 @@ class Viewer(qt.QMainWindow):
return dialog
def about(self):
- import silx._version
- message = """<p align="center"><b>Silx viewer</b>
- <br />
- <br />{silx_version}
- <br />
- <br /><a href="{project_url}">Upstream project on GitHub</a>
- </p>
- <p align="left">
- <dl>
- <dt><b>Silx version</b></dt><dd>{silx_version}</dd>
- <dt><b>Qt version</b></dt><dd>{qt_version}</dd>
- <dt><b>Qt binding</b></dt><dd>{qt_binding}</dd>
- <dt><b>Python version</b></dt><dd>{python_version}</dd>
- <dt><b>Optional libraries</b></dt><dd>{optional_lib}</dd>
- </dl>
- </p>
- <p>
- Copyright (C) <a href="{esrf_url}">European Synchrotron Radiation Facility</a>
- </p>
- """
- def format_optional_lib(name, isAvailable):
- if isAvailable:
- template = '<b>%s</b> is <font color="green">installed</font>'
- else:
- template = '<b>%s</b> is <font color="red">not installed</font>'
- return template % name
-
- optional_lib = []
- optional_lib.append(format_optional_lib("FabIO", fabio is not None))
- optional_lib.append(format_optional_lib("H5py", h5py is not None))
- optional_lib.append(format_optional_lib("hdf5plugin", hdf5plugin is not None))
-
- info = dict(
- esrf_url="http://www.esrf.eu",
- project_url="https://github.com/silx-kit/silx",
- silx_version=silx._version.version,
- qt_binding=qt.BINDING,
- qt_version=qt.qVersion(),
- python_version=sys.version.replace("\n", "<br />"),
- optional_lib="<br />".join(optional_lib)
- )
- qt.QMessageBox.about(self, "About Menu", message.format(**info))
+ from . import qtutils
+ qtutils.About.about(self, "Silx viewer")
def appendFile(self, filename):
self.__treeview.findHdf5TreeModel().appendFile(filename)
@@ -229,7 +176,7 @@ class Viewer(qt.QMainWindow):
def displayData(self):
"""Called to update the dataviewer with the selected data.
"""
- selected = list(self.__treeview.selectedH5Nodes())
+ selected = list(self.__treeview.selectedH5Nodes(ignoreBrokenLinks=False))
if len(selected) == 1:
# Update the viewer for a single selection
data = selected[0]
@@ -238,40 +185,20 @@ class Viewer(qt.QMainWindow):
def useAsyncLoad(self, useAsync):
self.__asyncload = useAsync
- def customContextMenu(self, event):
- """Called to populate the context menu
-
- :param silx.gui.hdf5.Hdf5ContextMenuEvent event: Event
- containing expected information to populate the context menu
- """
- selectedObjects = event.source().selectedH5Nodes()
- menu = event.menu()
-
- hasDataset = False
- for obj in selectedObjects:
- if obj.ntype is h5py.Dataset:
- hasDataset = True
- break
-
- if len(menu.children()):
- menu.addSeparator()
-
- if hasDataset:
- action = qt.QAction("Do something on the datasets", event.source())
- menu.addAction(action)
-
def closeAndSyncCustomContextMenu(self, event):
"""Called to populate the context menu
:param silx.gui.hdf5.Hdf5ContextMenuEvent event: Event
containing expected information to populate the context menu
"""
- selectedObjects = event.source().selectedH5Nodes()
+ selectedObjects = event.source().selectedH5Nodes(ignoreBrokenLinks=False)
menu = event.menu()
if len(menu.children()):
menu.addSeparator()
+ # Import it here to be sure to use the right logging level
+ import h5py
for obj in selectedObjects:
if obj.ntype is h5py.File:
action = qt.QAction("Remove %s" % obj.local_filename, event.source())
@@ -292,12 +219,43 @@ def main(argv):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
'files',
- type=argparse.FileType('rb'),
nargs=argparse.ZERO_OR_MORE,
help='Data file to show (h5 file, edf files, spec files)')
+ parser.add_argument(
+ '--debug',
+ dest="debug",
+ action="store_true",
+ default=False,
+ help='Set logging system in debug mode')
+ parser.add_argument(
+ '--use-opengl-plot',
+ dest="use_opengl_plot",
+ action="store_true",
+ default=False,
+ help='Use OpenGL for plots (instead of matplotlib)')
options = parser.parse_args(argv[1:])
+ if options.debug:
+ logging.root.setLevel(logging.DEBUG)
+
+ #
+ # Import most of the things here to be sure to use the right logging level
+ #
+
+ try:
+ # it should be loaded before h5py
+ import hdf5plugin # noqa
+ except ImportError:
+ _logger.debug("Backtrace", exc_info=True)
+ hdf5plugin = None
+
+ try:
+ import h5py
+ except ImportError:
+ _logger.debug("Backtrace", exc_info=True)
+ h5py = None
+
if h5py is None:
message = "Module 'h5py' is not installed but is mandatory."\
+ " You can install it using \"pip install h5py\"."
@@ -309,15 +267,27 @@ def main(argv):
+ " compressions. You can install it using \"pip install hdf5plugin\"."
_logger.warning(message)
+ #
+ # Run the application
+ #
+
+ if options.use_opengl_plot:
+ from silx.gui.plot import PlotWidget
+ PlotWidget.setDefaultBackend("opengl")
+
app = qt.QApplication([])
+ qt.QLocale.setDefault(qt.QLocale.c())
+
sys.excepthook = qt.exceptionHandler
window = Viewer()
window.resize(qt.QSize(640, 480))
- for f in options.files:
- filename = f.name
- f.close()
- window.appendFile(filename)
+ for filename in options.files:
+ try:
+ window.appendFile(filename)
+ except IOError as e:
+ _logger.error(e.args[0])
+ _logger.debug("Backtrace", exc_info=True)
window.show()
result = app.exec_()
diff --git a/silx/gui/_glutils/FramebufferTexture.py b/silx/gui/_glutils/FramebufferTexture.py
index b01eb41..cc05080 100644
--- a/silx/gui/_glutils/FramebufferTexture.py
+++ b/silx/gui/_glutils/FramebufferTexture.py
@@ -66,49 +66,48 @@ class FramebufferTexture(object):
self._previousFramebuffer = 0 # Used by with statement
self._name = gl.glGenFramebuffers(1)
- gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self._name)
-
- # Attachments
- gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER,
- gl.GL_COLOR_ATTACHMENT0,
- gl.GL_TEXTURE_2D,
- self._texture.name,
- 0)
-
- height, width = self._texture.shape
-
- if stencilFormat is not None:
- self._stencilId = gl.glGenRenderbuffers(1)
- gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, self._stencilId)
- gl.glRenderbufferStorage(gl.GL_RENDERBUFFER,
- stencilFormat,
- width, height)
- gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER,
- gl.GL_STENCIL_ATTACHMENT,
- gl.GL_RENDERBUFFER,
- self._stencilId)
- else:
- self._stencilId = None
- if depthFormat is not None:
- if self._stencilId and depthFormat in self._PACKED_FORMAT:
- self._depthId = self._stencilId
- else:
- self._depthId = gl.glGenRenderbuffers(1)
- gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, self._depthId)
+ with self: # Bind FBO
+ # Attachments
+ gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER,
+ gl.GL_COLOR_ATTACHMENT0,
+ gl.GL_TEXTURE_2D,
+ self._texture.name,
+ 0)
+
+ height, width = self._texture.shape
+
+ if stencilFormat is not None:
+ self._stencilId = gl.glGenRenderbuffers(1)
+ gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, self._stencilId)
gl.glRenderbufferStorage(gl.GL_RENDERBUFFER,
- depthFormat,
+ stencilFormat,
width, height)
- gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER,
- gl.GL_DEPTH_ATTACHMENT,
- gl.GL_RENDERBUFFER,
- self._depthId)
- else:
- self._depthId = None
+ gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER,
+ gl.GL_STENCIL_ATTACHMENT,
+ gl.GL_RENDERBUFFER,
+ self._stencilId)
+ else:
+ self._stencilId = None
+
+ if depthFormat is not None:
+ if self._stencilId and depthFormat in self._PACKED_FORMAT:
+ self._depthId = self._stencilId
+ else:
+ self._depthId = gl.glGenRenderbuffers(1)
+ gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, self._depthId)
+ gl.glRenderbufferStorage(gl.GL_RENDERBUFFER,
+ depthFormat,
+ width, height)
+ gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER,
+ gl.GL_DEPTH_ATTACHMENT,
+ gl.GL_RENDERBUFFER,
+ self._depthId)
+ else:
+ self._depthId = None
- assert gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) == \
- gl.GL_FRAMEBUFFER_COMPLETE
- gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
+ assert (gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) ==
+ gl.GL_FRAMEBUFFER_COMPLETE)
@property
def shape(self):
@@ -143,6 +142,7 @@ class FramebufferTexture(object):
def __exit__(self, exctype, excvalue, traceback):
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self._previousFramebuffer)
+ self._previousFramebuffer = None
def discard(self):
"""Delete associated OpenGL resources including texture"""
diff --git a/silx/gui/_glutils/OpenGLWidget.py b/silx/gui/_glutils/OpenGLWidget.py
new file mode 100644
index 0000000..6cbf8f0
--- /dev/null
+++ b/silx/gui/_glutils/OpenGLWidget.py
@@ -0,0 +1,409 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2017 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This package provides a compatibility layer for OpenGL widget.
+
+It provides a compatibility layer for Qt OpenGL widget used in silx
+across Qt<=5.3 QtOpenGL.QGLWidget and QOpenGLWidget.
+"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "26/07/2017"
+
+
+import logging
+import sys
+
+from .. import qt
+from .._glutils import gl
+
+
+_logger = logging.getLogger(__name__)
+
+
+# Probe OpenGL availability and widget
+ERROR = '' # Error message from probing Qt OpenGL support
+_BaseOpenGLWidget = None # Qt OpenGL widget to use
+
+if hasattr(qt, 'QOpenGLWidget'): # PyQt>=5.4
+ _logger.info('Using QOpenGLWidget')
+ _BaseOpenGLWidget = qt.QOpenGLWidget
+
+elif not qt.HAS_OPENGL: # QtOpenGL not installed
+ ERROR = '%s.QtOpenGL not available' % qt.BINDING
+
+elif qt.QApplication.instance() and not qt.QGLFormat.hasOpenGL():
+ # qt.QGLFormat.hasOpenGL MUST be called with a QApplication created
+ # so this is only checked if the QApplication is already created
+ ERROR = 'Qt reports OpenGL not available'
+
+else:
+ _logger.info('Using QGLWidget')
+ _BaseOpenGLWidget = qt.QGLWidget
+
+
+# Internal class wrapping Qt OpenGL widget
+if _BaseOpenGLWidget is None:
+ _logger.error('OpenGL-based widget disabled: %s', ERROR)
+ _OpenGLWidget = None
+
+else:
+ class _OpenGLWidget(_BaseOpenGLWidget):
+ """Wrapper over QOpenGLWidget and QGLWidget"""
+
+ sigOpenGLContextError = qt.Signal(str)
+ """Signal emitted when an OpenGL context error is detected at runtime.
+
+ It provides the error reason as a str.
+ """
+
+ def __init__(self, parent,
+ alphaBufferSize=0,
+ depthBufferSize=24,
+ stencilBufferSize=8,
+ version=(2, 0),
+ f=qt.Qt.WindowFlags()):
+ # True if using QGLWidget, False if using QOpenGLWidget
+ self.__legacy = not hasattr(qt, 'QOpenGLWidget')
+
+ self.__devicePixelRatio = 1.0
+ self.__requestedOpenGLVersion = int(version[0]), int(version[1])
+ self.__isValid = False
+
+ if self.__legacy: # QGLWidget
+ format_ = qt.QGLFormat()
+ format_.setAlphaBufferSize(alphaBufferSize)
+ format_.setAlpha(alphaBufferSize != 0)
+ format_.setDepthBufferSize(depthBufferSize)
+ format_.setDepth(depthBufferSize != 0)
+ format_.setStencilBufferSize(stencilBufferSize)
+ format_.setStencil(stencilBufferSize != 0)
+ format_.setVersion(*self.__requestedOpenGLVersion)
+ format_.setDoubleBuffer(True)
+
+ super(_OpenGLWidget, self).__init__(format_, parent, None, f)
+
+ else: # QOpenGLWidget
+ super(_OpenGLWidget, self).__init__(parent, f)
+
+ format_ = qt.QSurfaceFormat()
+ format_.setAlphaBufferSize(alphaBufferSize)
+ format_.setDepthBufferSize(depthBufferSize)
+ format_.setStencilBufferSize(stencilBufferSize)
+ format_.setVersion(*self.__requestedOpenGLVersion)
+ format_.setSwapBehavior(qt.QSurfaceFormat.DoubleBuffer)
+ self.setFormat(format_)
+
+
+ def getDevicePixelRatio(self):
+ """Returns the ratio device-independent / device pixel size
+
+ It should be either 1.0 or 2.0.
+
+ :return: Scale factor between screen and Qt units
+ :rtype: float
+ """
+ return self.__devicePixelRatio
+
+ def getRequestedOpenGLVersion(self):
+ """Returns the requested OpenGL version.
+
+ :return: (major, minor)
+ :rtype: 2-tuple of int"""
+ return self.__requestedOpenGLVersion
+
+ def getOpenGLVersion(self):
+ """Returns the available OpenGL version.
+
+ :return: (major, minor)
+ :rtype: 2-tuple of int"""
+ if self.__legacy: # QGLWidget
+ supportedVersion = 0, 0
+
+ # Go through all OpenGL version flags checking support
+ flags = self.format().openGLVersionFlags()
+ for version in ((1, 1), (1, 2), (1, 3), (1, 4), (1, 5),
+ (2, 0), (2, 1),
+ (3, 0), (3, 1), (3, 2), (3, 3),
+ (4, 0)):
+ versionFlag = getattr(qt.QGLFormat,
+ 'OpenGL_Version_%d_%d' % version)
+ if not versionFlag & flags:
+ break
+ supportedVersion = version
+ return supportedVersion
+
+ else: # QOpenGLWidget
+ return self.format().version()
+
+ # QOpenGLWidget methods
+
+ def isValid(self):
+ """Returns True if OpenGL is available.
+
+ This adds extra checks to Qt isValid method.
+
+ :rtype: bool
+ """
+ return self.__isValid and super(_OpenGLWidget, self).isValid()
+
+ def defaultFramebufferObject(self):
+ """Returns the framebuffer object handle.
+
+ See :meth:`QOpenGLWidget.defaultFramebufferObject`
+ """
+ if self.__legacy: # QGLWidget
+ return 0
+ else: # QOpenGLWidget
+ return super(_OpenGLWidget, self).defaultFramebufferObject()
+
+ # *GL overridden methods
+
+ def initializeGL(self):
+ parent = self.parent()
+ if parent is None:
+ _logger.error('_OpenGLWidget has no parent')
+ return
+
+ # Check OpenGL version
+ if self.getOpenGLVersion() >= self.getRequestedOpenGLVersion():
+ version = gl.glGetString(gl.GL_VERSION)
+ if version:
+ self.__isValid = True
+ else:
+ errMsg = 'OpenGL not available'
+ if sys.platform.startswith('linux'):
+ errMsg += ': If connected remotely, ' \
+ 'GLX forwarding might be disabled.'
+ _logger.error(errMsg)
+ self.sigOpenGLContextError.emit(errMsg)
+ self.__isValid = False
+
+ else:
+ errMsg = 'OpenGL %d.%d not available' % \
+ self.getRequestedOpenGLVersion()
+ _logger.error('OpenGL widget disabled: %s', errMsg)
+ self.sigOpenGLContextError.emit(errMsg)
+ self.__isValid = False
+
+ if self.isValid():
+ parent.initializeGL()
+
+ def paintGL(self):
+ parent = self.parent()
+ if parent is None:
+ _logger.error('_OpenGLWidget has no parent')
+ return
+
+ if qt.BINDING == 'PyQt5':
+ devicePixelRatio = self.window().windowHandle().devicePixelRatio()
+
+ if devicePixelRatio != self.getDevicePixelRatio():
+ # Update devicePixelRatio and call resizeOpenGL
+ # as resizeGL is not always called.
+ self.__devicePixelRatio = devicePixelRatio
+ self.makeCurrent()
+ parent.resizeGL(self.width(), self.height())
+
+ if self.isValid():
+ parent.paintGL()
+
+ def resizeGL(self, width, height):
+ parent = self.parent()
+ if parent is None:
+ _logger.error('_OpenGLWidget has no parent')
+ return
+
+ if self.isValid():
+ # Call parent resizeGL with device-independent pixel unit
+ # This works over both QGLWidget and QOpenGLWidget
+ parent.resizeGL(self.width(), self.height())
+
+
+class OpenGLWidget(qt.QWidget):
+ """OpenGL widget wrapper over QGLWidget and QOpenGLWidget
+
+ This wrapper API implements a subset of QOpenGLWidget API.
+ The constructor takes a different set of arguments.
+ Methods returning object like :meth:`context` returns either
+ QGL* or QOpenGL* objects.
+
+ :param parent: Parent widget see :class:`QWidget`
+ :param int alphaBufferSize:
+ Size in bits of the alpha channel (default: 0).
+ Set to 0 to disable alpha channel.
+ :param int depthBufferSize:
+ Size in bits of the depth buffer (default: 24).
+ Set to 0 to disable depth buffer.
+ :param int stencilBufferSize:
+ Size in bits of the stencil buffer (default: 8).
+ Set to 0 to disable stencil buffer
+ :param version: Requested OpenGL version (default: (2, 0)).
+ :type version: 2-tuple of int
+ :param f: see :class:`QWidget`
+ """
+
+ def __init__(self, parent=None,
+ alphaBufferSize=0,
+ depthBufferSize=24,
+ stencilBufferSize=8,
+ version=(2, 0),
+ f=qt.Qt.WindowFlags()):
+ super(OpenGLWidget, self).__init__(parent, f)
+
+ layout = qt.QHBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ self.setLayout(layout)
+
+ if _OpenGLWidget is None:
+ self.__openGLWidget = None
+ label = self._createErrorQLabel(ERROR)
+ self.layout().addWidget(label)
+
+ else:
+ self.__openGLWidget = _OpenGLWidget(
+ parent=self,
+ alphaBufferSize=alphaBufferSize,
+ depthBufferSize=depthBufferSize,
+ stencilBufferSize=stencilBufferSize,
+ version=version,
+ f=f)
+ # Async connection need, otherwise issue when hiding OpenGL
+ # widget while doing the rendering..
+ self.__openGLWidget.sigOpenGLContextError.connect(
+ self._handleOpenGLInitError, qt.Qt.QueuedConnection)
+ self.layout().addWidget(self.__openGLWidget)
+
+ @staticmethod
+ def _createErrorQLabel(error):
+ """Create QLabel displaying error message in place of OpenGL widget
+
+ :param str error: The error message to display"""
+ label = qt.QLabel()
+ label.setText('OpenGL-based widget disabled:\n%s' % error)
+ label.setAlignment(qt.Qt.AlignCenter)
+ label.setWordWrap(True)
+ return label
+
+ def _handleOpenGLInitError(self, error):
+ """Handle runtime errors in OpenGL widget"""
+ if self.__openGLWidget is not None:
+ self.__openGLWidget.setVisible(False)
+ self.__openGLWidget.setParent(None)
+ self.__openGLWidget = None
+
+ label = self._createErrorQLabel(error)
+ self.layout().addWidget(label)
+
+ # Additional API
+
+ def getDevicePixelRatio(self):
+ """Returns the ratio device-independent / device pixel size
+
+ It should be either 1.0 or 2.0.
+
+ :return: Scale factor between screen and Qt units
+ :rtype: float
+ """
+ if self.__openGLWidget is None:
+ return 1.
+ else:
+ return self.__openGLWidget.getDevicePixelRatio()
+
+ def getOpenGLVersion(self):
+ """Returns the available OpenGL version.
+
+ :return: (major, minor)
+ :rtype: 2-tuple of int"""
+ if self.__openGLWidget is None:
+ return 0, 0
+ else:
+ return self.__openGLWidget.getOpenGLVersion()
+
+ # QOpenGLWidget API
+
+ def isValid(self):
+ """Returns True if OpenGL with the requested version is available.
+
+ :rtype: bool
+ """
+ if self.__openGLWidget is None:
+ return False
+ else:
+ return self.__openGLWidget.isValid()
+
+ def context(self):
+ """Return Qt OpenGL context object or None.
+
+ See :meth:`QOpenGLWidget.context` and :meth:`QGLWidget.context`
+ """
+ if self.__openGLWidget is None:
+ return None
+ else:
+ return self.__openGLWidget.context()
+
+ def defaultFramebufferObject(self):
+ """Returns the framebuffer object handle.
+
+ See :meth:`QOpenGLWidget.defaultFramebufferObject`
+ """
+ if self.__openGLWidget is None:
+ return 0
+ else:
+ return self.__openGLWidget.defaultFramebufferObject()
+
+ def makeCurrent(self):
+ """Make the underlying OpenGL widget's context current.
+
+ See :meth:`QOpenGLWidget.makeCurrent`
+ """
+ if self.__openGLWidget is not None:
+ self.__openGLWidget.makeCurrent()
+
+ def update(self):
+ """Async update of the OpenGL widget.
+
+ See :meth:`QOpenGLWidget.update`
+ """
+ if self.__openGLWidget is not None:
+ self.__openGLWidget.update()
+
+ # QOpenGLWidget API to override
+
+ def initializeGL(self):
+ """Override to implement OpenGL initialization."""
+ pass
+
+ def paintGL(self):
+ """Override to implement OpenGL rendering."""
+ pass
+
+ def resizeGL(self, width, height):
+ """Override to implement resize of OpenGL framebuffer.
+
+ :param int width: Width in device-independent pixels
+ :param int height: Height in device-independent pixels
+ """
+ pass
diff --git a/silx/gui/_glutils/VertexBuffer.py b/silx/gui/_glutils/VertexBuffer.py
index 689b543..b74b748 100644
--- a/silx/gui/_glutils/VertexBuffer.py
+++ b/silx/gui/_glutils/VertexBuffer.py
@@ -180,7 +180,7 @@ class VertexBufferAttrib(object):
dimension=1,
offset=0,
stride=0,
- normalisation=False):
+ normalization=False):
self.vbo = vbo
assert type_ in self._GL_TYPES
self.type_ = type_
@@ -189,7 +189,7 @@ class VertexBufferAttrib(object):
self.dimension = dimension
self.offset = offset
self.stride = stride
- self.normalisation = bool(normalisation)
+ self.normalization = bool(normalization)
@property
def itemsize(self):
@@ -200,12 +200,12 @@ class VertexBufferAttrib(object):
def setVertexAttrib(self, attribute):
"""Call glVertexAttribPointer with objects information"""
- normalisation = gl.GL_TRUE if self.normalisation else gl.GL_FALSE
+ normalization = gl.GL_TRUE if self.normalization else gl.GL_FALSE
with self.vbo:
gl.glVertexAttribPointer(attribute,
self.dimension,
self.type_,
- normalisation,
+ normalization,
self.stride,
c_void_p(self.offset))
@@ -216,7 +216,7 @@ class VertexBufferAttrib(object):
self.dimension,
self.offset,
self.stride,
- self.normalisation)
+ self.normalization)
def vertexBuffer(arrays, prefix=None, suffix=None, usage=None):
diff --git a/silx/gui/_glutils/__init__.py b/silx/gui/_glutils/__init__.py
index e86a58f..15e48e1 100644
--- a/silx/gui/_glutils/__init__.py
+++ b/silx/gui/_glutils/__init__.py
@@ -33,6 +33,7 @@ __date__ = "25/07/2016"
# OpenGL convenient functions
+from .OpenGLWidget import OpenGLWidget # noqa
from .Context import getGLContext, setGLContextGetter # noqa
from .FramebufferTexture import FramebufferTexture # noqa
from .Program import Program # noqa
diff --git a/silx/gui/_glutils/font.py b/silx/gui/_glutils/font.py
index 566ae49..2be2c04 100644
--- a/silx/gui/_glutils/font.py
+++ b/silx/gui/_glutils/font.py
@@ -98,27 +98,39 @@ def rasterText(text, font,
_logger.info("Trying to raster empty text, replaced by white space")
text = ' ' # Replace empty text by white space to produce an image
+ if (devicePixelRatio != 1.0 and
+ not hasattr(qt.QImage, 'setDevicePixelRatio')): # Qt 4
+ _logger.error('devicePixelRatio not supported')
+ devicePixelRatio = 1.0
+
if not isinstance(font, qt.QFont):
font = qt.QFont(font, size, weight, italic)
+ # get text size
+ image = qt.QImage(1, 1, qt.QImage.Format_RGB888)
+ painter = qt.QPainter()
+ painter.begin(image)
+ painter.setPen(qt.Qt.white)
+ painter.setFont(font)
+ bounds = painter.boundingRect(
+ qt.QRect(0, 0, 4096, 4096), qt.Qt.TextExpandTabs, text)
+ painter.end()
+
metrics = qt.QFontMetrics(font)
- size = metrics.size(qt.Qt.TextExpandTabs, text)
- bounds = metrics.boundingRect(
- qt.QRect(0, 0, size.width(), size.height()),
- qt.Qt.TextExpandTabs,
- text)
- if (devicePixelRatio != 1.0 and
- not hasattr(qt.QImage, 'setDevicePixelRatio')): # Qt 4
- _logger.error('devicePixelRatio not supported')
- devicePixelRatio = 1.0
+ # This does not provide the correct text bbox on macOS
+ # size = metrics.size(qt.Qt.TextExpandTabs, text)
+ # bounds = metrics.boundingRect(
+ # qt.QRect(0, 0, size.width(), size.height()),
+ # qt.Qt.TextExpandTabs,
+ # text)
# Add extra border and handle devicePixelRatio
width = bounds.width() * devicePixelRatio + 2
# align line size to 32 bits to ease conversion to numpy array
width = 4 * ((width + 3) // 4)
image = qt.QImage(width,
- bounds.height() * devicePixelRatio,
+ bounds.height() * devicePixelRatio + 2,
qt.QImage.Format_RGB888)
if (devicePixelRatio != 1.0 and
hasattr(image, 'setDevicePixelRatio')): # Qt 5
diff --git a/silx/gui/console.py b/silx/gui/console.py
index 13760b4..7812e2d 100644
--- a/silx/gui/console.py
+++ b/silx/gui/console.py
@@ -136,6 +136,8 @@ if qtconsole is None:
class IPythonWidget(RichIPythonWidget):
"""Live IPython console widget.
+ .. image:: img/IPythonWidget.png
+
:param custom_banner: Custom welcome message to be printed at the top of
the console.
"""
@@ -175,6 +177,8 @@ class IPythonDockWidget(qt.QDockWidget):
"""Dock Widget including a :class:`IPythonWidget` inside
a vertical layout.
+ .. image:: img/IPythonDockWidget.png
+
:param available_vars: Dictionary of variables to be pushed to the
console's interactive namespace: ``{"variable_name": object, …}``
:param custom_banner: Custom welcome message to be printed at the top of
diff --git a/silx/gui/data/ArrayTableModel.py b/silx/gui/data/ArrayTableModel.py
index 87a2fc1..ad4d33a 100644
--- a/silx/gui/data/ArrayTableModel.py
+++ b/silx/gui/data/ArrayTableModel.py
@@ -34,7 +34,7 @@ from silx.gui.data.TextFormatter import TextFormatter
__authors__ = ["V.A. Sole"]
__license__ = "MIT"
-__date__ = "24/01/2017"
+__date__ = "27/09/2017"
_logger = logging.getLogger(__name__)
@@ -191,7 +191,7 @@ class ArrayTableModel(qt.QAbstractTableModel):
selection = self._getIndexTuple(index.row(),
index.column())
if role == qt.Qt.DisplayRole:
- return self._formatter.toString(self._array[selection])
+ return self._formatter.toString(self._array[selection], self._array.dtype)
if role == qt.Qt.BackgroundRole and self._bgcolors is not None:
r, g, b = self._bgcolors[selection][0:3]
@@ -296,6 +296,9 @@ class ArrayTableModel(qt.QAbstractTableModel):
elif copy:
# copy requested (default)
self._array = numpy.array(data, copy=True)
+ if hasattr(data, "dtype"):
+ # Avoid to lose the monkey-patched h5py dtype
+ self._array.dtype = data.dtype
elif not _is_array(data):
raise TypeError("data is not a proper array. Try setting" +
" copy=True to convert it into a numpy array" +
diff --git a/silx/gui/data/ArrayTableWidget.py b/silx/gui/data/ArrayTableWidget.py
index ba3fa11..cb8e915 100644
--- a/silx/gui/data/ArrayTableWidget.py
+++ b/silx/gui/data/ArrayTableWidget.py
@@ -230,6 +230,8 @@ class ArrayTableWidget(qt.QWidget):
To select the perspective, use :meth:`setPerspective` or
use :meth:`setFrameAxes`.
To select the frame, use :meth:`setFrameIndex`.
+
+ .. image:: img/ArrayTableWidget.png
"""
def __init__(self, parent=None):
"""
diff --git a/silx/gui/data/DataViewer.py b/silx/gui/data/DataViewer.py
index 3a3ac64..750c654 100644
--- a/silx/gui/data/DataViewer.py
+++ b/silx/gui/data/DataViewer.py
@@ -22,8 +22,8 @@
# THE SOFTWARE.
#
# ###########################################################################*/
-"""This module defines a widget designed to display data using to most adapted
-view from available ones from silx.
+"""This module defines a widget designed to display data using the most adapted
+view from the ones provided by silx.
"""
from __future__ import division
@@ -35,7 +35,7 @@ from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "26/04/2017"
+__date__ = "03/10/2017"
_logger = logging.getLogger(__name__)
@@ -144,7 +144,7 @@ class DataViewer(qt.QFrame):
DataViews._Hdf5View,
DataViews._NXdataView,
DataViews._Plot1dView,
- DataViews._Plot2dView,
+ DataViews._ImageView,
DataViews._Plot3dView,
DataViews._RawView,
DataViews._StackView,
@@ -201,7 +201,7 @@ class DataViewer(qt.QFrame):
self.__numpySelection.clear()
info = DataViews.DataInfo(self.__data)
axisNames = self.__currentView.axesNames(self.__data, info)
- if info.isArray and self.__data is not None and len(axisNames) > 0:
+ if info.isArray and info.size != 0 and self.__data is not None and axisNames is not None:
self.__useAxisSelection = True
self.__numpySelection.setAxisNames(axisNames)
self.__numpySelection.setCustomAxis(self.__currentView.customAxisNames())
diff --git a/silx/gui/data/DataViewerFrame.py b/silx/gui/data/DataViewerFrame.py
index b48fa7b..e050d4a 100644
--- a/silx/gui/data/DataViewerFrame.py
+++ b/silx/gui/data/DataViewerFrame.py
@@ -27,7 +27,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "10/04/2017"
+__date__ = "21/09/2017"
from silx.gui import qt
from .DataViewer import DataViewer
@@ -79,6 +79,14 @@ class DataViewerFrame(qt.QWidget):
"""Avoid to create views while the instance is not created."""
super(_DataViewer, self)._initializeViews()
+ def _createDefaultViews(self, parent):
+ """Expose the original `createDefaultViews` function"""
+ return super(_DataViewer, self).createDefaultViews()
+
+ def createDefaultViews(self, parent=None):
+ """Allow the DataViewerFrame to override this function"""
+ return self.parent().createDefaultViews(parent)
+
self.__dataViewer = _DataViewer(self)
# initialize views when `self.__dataViewer` is set
self.__dataViewer.initializeViews()
@@ -127,7 +135,7 @@ class DataViewerFrame(qt.QWidget):
:param QWidget parent: QWidget parent of the views
:rtype: list[silx.gui.data.DataViews.DataView]
"""
- return self.__dataViewer.createDefaultViews(parent)
+ return self.__dataViewer._createDefaultViews(parent)
def addView(self, view):
"""Allow to add a view to the dataview.
diff --git a/silx/gui/data/DataViews.py b/silx/gui/data/DataViews.py
index d8d605a..1ad997b 100644
--- a/silx/gui/data/DataViews.py
+++ b/silx/gui/data/DataViews.py
@@ -25,6 +25,7 @@
"""This module defines a views used by :class:`silx.gui.data.DataViewer`.
"""
+from collections import OrderedDict
import logging
import numbers
import numpy
@@ -34,11 +35,11 @@ from silx.gui import qt, icons
from silx.gui.data.TextFormatter import TextFormatter
from silx.io import nxdata
from silx.gui.hdf5 import H5Node
-from silx.io.nxdata import NXdata
+from silx.io.nxdata import NXdata, get_attr_as_string
__authors__ = ["V. Valls", "P. Knobel"]
__license__ = "MIT"
-__date__ = "07/04/2017"
+__date__ = "03/10/2017"
_logger = logging.getLogger(__name__)
@@ -52,6 +53,7 @@ RAW_MODE = 40
RAW_ARRAY_MODE = 41
RAW_RECORD_MODE = 42
RAW_SCALAR_MODE = 43
+RAW_HEXA_MODE = 44
STACK_MODE = 50
HDF5_MODE = 60
@@ -62,6 +64,8 @@ def _normalizeData(data):
If the data embed a numpy data or a dataset it is returned.
Else returns the input data."""
if isinstance(data, H5Node):
+ if data.is_broken:
+ return None
return data.h5py_object
return data
@@ -89,11 +93,14 @@ class DataInfo(object):
self.isArray = False
self.interpretation = None
self.isNumeric = False
+ self.isVoid = False
self.isComplex = False
+ self.isBoolean = False
self.isRecord = False
self.isNXdata = False
self.shape = tuple()
self.dim = 0
+ self.size = 0
if data is None:
return
@@ -110,23 +117,32 @@ class DataInfo(object):
self.isArray = False
if silx.io.is_dataset(data):
- self.interpretation = data.attrs.get("interpretation", None)
+ if "interpretation" in data.attrs:
+ self.interpretation = get_attr_as_string(data, "interpretation")
+ else:
+ self.interpretation = None
elif self.isNXdata:
self.interpretation = nxd.interpretation
else:
self.interpretation = None
if hasattr(data, "dtype"):
+ if numpy.issubdtype(data.dtype, numpy.void):
+ # That's a real opaque type, else it is a structured type
+ self.isVoid = data.dtype.fields is None
self.isNumeric = numpy.issubdtype(data.dtype, numpy.number)
self.isRecord = data.dtype.fields is not None
self.isComplex = numpy.issubdtype(data.dtype, numpy.complex)
+ self.isBoolean = numpy.issubdtype(data.dtype, numpy.bool_)
elif self.isNXdata:
self.isNumeric = numpy.issubdtype(nxd.signal.dtype,
numpy.number)
self.isComplex = numpy.issubdtype(nxd.signal.dtype, numpy.complex)
+ self.isBoolean = numpy.issubdtype(nxd.signal.dtype, numpy.bool_)
else:
self.isNumeric = isinstance(data, numbers.Number)
self.isComplex = isinstance(data, numbers.Complex)
+ self.isBoolean = isinstance(data, bool)
self.isRecord = False
if hasattr(data, "shape"):
@@ -135,7 +151,13 @@ class DataInfo(object):
self.shape = nxd.signal.shape
else:
self.shape = tuple()
- self.dim = len(self.shape)
+ if self.shape is not None:
+ self.dim = len(self.shape)
+
+ if hasattr(data, "size"):
+ self.size = int(data.size)
+ else:
+ self.size = 1
def normalizeData(self, data):
"""Returns a normalized data if the embed a numpy or a dataset.
@@ -237,12 +259,12 @@ class DataView(object):
def axesNames(self, data, info):
"""Returns names of the expected axes of the view, according to the
- input data.
+ input data. A none value will disable the default axes selectior.
:param data: Data to display
:type data: numpy.ndarray or h5py.Dataset
:param DataInfo info: Pre-computed information on the data
- :rtype: list[str]
+ :rtype: list[str] or None
"""
return []
@@ -276,7 +298,7 @@ class CompositeDataView(DataView):
:param qt.QWidget parent: Parent of the hold widget
"""
super(CompositeDataView, self).__init__(parent, modeId, icon, label)
- self.__views = {}
+ self.__views = OrderedDict()
self.__currentView = None
def addView(self, dataView):
@@ -285,10 +307,9 @@ class CompositeDataView(DataView):
def getBestView(self, data, info):
"""Returns the best view according to priorities."""
- info = DataInfo(data)
views = [(v.getDataPriority(data, info), v) for v in self.__views.keys()]
views = filter(lambda t: t[0] > DataView.UNSUPPORTED, views)
- views = sorted(views, reverse=True)
+ views = sorted(views, key=lambda t: t[0], reverse=True)
if len(views) == 0:
return None
@@ -361,7 +382,7 @@ class _EmptyView(DataView):
DataView.__init__(self, parent, modeId=EMPTY_MODE)
def axesNames(self, data, info):
- return []
+ return None
def createWidget(self, parent):
return qt.QLabel(parent)
@@ -406,6 +427,8 @@ class _Plot1dView(DataView):
return ["y"]
def getDataPriority(self, data, info):
+ if info.size <= 0:
+ return DataView.UNSUPPORTED
if data is None or not info.isArray or not info.isNumeric:
return DataView.UNSUPPORTED
if info.dim < 1:
@@ -434,9 +457,10 @@ class _Plot2dView(DataView):
def createWidget(self, parent):
from silx.gui import plot
widget = plot.Plot2D(parent=parent)
+ widget.getIntensityHistogramAction().setVisible(True)
widget.setKeepDataAspectRatio(True)
- widget.setGraphXLabel('X')
- widget.setGraphYLabel('Y')
+ widget.getXAxis().setLabel('X')
+ widget.getYAxis().setLabel('Y')
return widget
def clear(self):
@@ -459,7 +483,11 @@ class _Plot2dView(DataView):
return ["y", "x"]
def getDataPriority(self, data, info):
- if data is None or not info.isArray or not info.isNumeric:
+ if info.size <= 0:
+ return DataView.UNSUPPORTED
+ if (data is None or
+ not info.isArray or
+ not (info.isNumeric or info.isBoolean)):
return DataView.UNSUPPORTED
if info.dim < 2:
return DataView.UNSUPPORTED
@@ -494,8 +522,15 @@ class _Plot3dView(DataView):
plot = ScalarFieldView.ScalarFieldView(parent)
plot.setAxesLabels(*reversed(self.axesNames(None, None)))
- plot.addIsosurface(
- lambda data: numpy.mean(data) + numpy.std(data), '#FF0000FF')
+
+ 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)
@@ -527,6 +562,8 @@ class _Plot3dView(DataView):
return ["z", "y", "x"]
def getDataPriority(self, data, info):
+ if info.size <= 0:
+ return DataView.UNSUPPORTED
if data is None or not info.isArray or not info.isNumeric:
return DataView.UNSUPPORTED
if info.dim < 3:
@@ -539,6 +576,54 @@ class _Plot3dView(DataView):
return 10
+class _ComplexImageView(DataView):
+ """View displaying data using a ComplexImageView"""
+
+ def __init__(self, parent):
+ super(_ComplexImageView, self).__init__(
+ parent=parent,
+ modeId=PLOT2D_MODE,
+ label="Complex Image",
+ icon=icons.getQIcon("view-2d"))
+
+ def createWidget(self, parent):
+ from silx.gui.plot.ComplexImageView import ComplexImageView
+ widget = ComplexImageView(parent=parent)
+ widget.getPlot().getIntensityHistogramAction().setVisible(True)
+ widget.getPlot().setKeepDataAspectRatio(True)
+ widget.getXAxis().setLabel('X')
+ widget.getYAxis().setLabel('Y')
+ return widget
+
+ def clear(self):
+ self.getWidget().setData(None)
+
+ def normalizeData(self, data):
+ data = DataView.normalizeData(self, data)
+ return data
+
+ def setData(self, data):
+ data = self.normalizeData(data)
+ self.getWidget().setData(data)
+
+ def axesNames(self, data, info):
+ return ["y", "x"]
+
+ def getDataPriority(self, data, info):
+ if info.size <= 0:
+ return DataView.UNSUPPORTED
+ if data is None or not info.isArray or not info.isComplex:
+ return DataView.UNSUPPORTED
+ if info.dim < 2:
+ return DataView.UNSUPPORTED
+ if info.interpretation == "image":
+ return 1000
+ if info.dim == 2:
+ return 200
+ else:
+ return 190
+
+
class _ArrayView(DataView):
"""View displaying data using a 2d table"""
@@ -562,6 +647,8 @@ class _ArrayView(DataView):
return ["col", "row"]
def getDataPriority(self, data, info):
+ if info.size <= 0:
+ return DataView.UNSUPPORTED
if data is None or not info.isArray or info.isRecord:
return DataView.UNSUPPORTED
if info.dim < 2:
@@ -618,6 +705,8 @@ class _StackView(DataView):
return ["depth", "y", "x"]
def getDataPriority(self, data, info):
+ if info.size <= 0:
+ return DataView.UNSUPPORTED
if data is None or not info.isArray or not info.isNumeric:
return DataView.UNSUPPORTED
if info.dim < 3:
@@ -644,17 +733,21 @@ class _ScalarView(DataView):
self.getWidget().setText("")
def setData(self, data):
- data = self.normalizeData(data)
- if silx.io.is_dataset(data):
- data = data[()]
- text = self.__formatter.toString(data)
+ d = self.normalizeData(data)
+ if silx.io.is_dataset(d):
+ d = d[()]
+ text = self.__formatter.toString(d, data.dtype)
self.getWidget().setText(text)
def axesNames(self, data, info):
return []
def getDataPriority(self, data, info):
+ if info.size <= 0:
+ return DataView.UNSUPPORTED
data = self.normalizeData(data)
+ if info.shape is None:
+ return DataView.UNSUPPORTED
if data is None:
return DataView.UNSUPPORTED
if silx.io.is_group(data):
@@ -681,13 +774,16 @@ class _RecordView(DataView):
data = self.normalizeData(data)
widget = self.getWidget()
widget.setArrayData(data)
- widget.resizeRowsToContents()
- widget.resizeColumnsToContents()
+ if len(data) < 100:
+ widget.resizeRowsToContents()
+ widget.resizeColumnsToContents()
def axesNames(self, data, info):
return ["data"]
def getDataPriority(self, data, info):
+ if info.size <= 0:
+ return DataView.UNSUPPORTED
if info.isRecord:
return 40
if data is None or not info.isArray:
@@ -703,6 +799,36 @@ class _RecordView(DataView):
return DataView.UNSUPPORTED
+class _HexaView(DataView):
+ """View displaying data using text"""
+
+ def __init__(self, parent):
+ DataView.__init__(self, parent, modeId=RAW_HEXA_MODE)
+
+ def createWidget(self, parent):
+ from .HexaTableView import HexaTableView
+ widget = HexaTableView(parent)
+ return widget
+
+ def clear(self):
+ self.getWidget().setArrayData(None)
+
+ def setData(self, data):
+ data = self.normalizeData(data)
+ widget = self.getWidget()
+ widget.setArrayData(data)
+
+ def axesNames(self, data, info):
+ return []
+
+ def getDataPriority(self, data, info):
+ if info.size <= 0:
+ return DataView.UNSUPPORTED
+ if info.isVoid:
+ return 2000
+ return DataView.UNSUPPORTED
+
+
class _Hdf5View(DataView):
"""View displaying data using text"""
@@ -727,7 +853,7 @@ class _Hdf5View(DataView):
widget.setData(data)
def axesNames(self, data, info):
- return []
+ return None
def getDataPriority(self, data, info):
widget = self.getWidget()
@@ -750,11 +876,28 @@ class _RawView(CompositeDataView):
modeId=RAW_MODE,
label="Raw",
icon=icons.getQIcon("view-raw"))
+ self.addView(_HexaView(parent))
self.addView(_ScalarView(parent))
self.addView(_ArrayView(parent))
self.addView(_RecordView(parent))
+class _ImageView(CompositeDataView):
+ """View displaying data as 2D image
+
+ It choose between Plot2D and ComplexImageView widgets
+ """
+
+ def __init__(self, parent):
+ super(_ImageView, self).__init__(
+ parent=parent,
+ modeId=PLOT2D_MODE,
+ label="Image",
+ icon=icons.getQIcon("view-2d"))
+ self.addView(_ComplexImageView(parent))
+ self.addView(_Plot2dView(parent))
+
+
class _NXdataScalarView(DataView):
"""DataView using a table view for displaying NXdata scalars:
0-D signal or n-D signal with *@interpretation=scalar*"""
@@ -806,7 +949,7 @@ class _NXdataCurveView(DataView):
def axesNames(self, data, info):
# disabled (used by default axis selector widget in Hdf5Viewer)
- return []
+ return None
def clear(self):
self.getWidget().clear()
@@ -814,10 +957,10 @@ class _NXdataCurveView(DataView):
def setData(self, data):
data = self.normalizeData(data)
nxd = NXdata(data)
- signal_name = data.attrs["signal"]
+ signal_name = get_attr_as_string(data, "signal")
group_name = data.name
- if nxd.axes_names[-1] is not None:
- x_errors = nxd.get_axis_errors(nxd.axes_names[-1])
+ if nxd.axes_dataset_names[-1] is not None:
+ x_errors = nxd.get_axis_errors(nxd.axes_dataset_names[-1])
else:
x_errors = None
@@ -853,7 +996,7 @@ class _NXdataXYVScatterView(DataView):
def axesNames(self, data, info):
# disabled (used by default axis selector widget in Hdf5Viewer)
- return []
+ return None
def clear(self):
self.getWidget().clear()
@@ -861,7 +1004,7 @@ class _NXdataXYVScatterView(DataView):
def setData(self, data):
data = self.normalizeData(data)
nxd = NXdata(data)
- signal_name = data.attrs["signal"]
+ signal_name = get_attr_as_string(data, "signal")
# signal_errors = nx.errors # not supported
group_name = data.name
x_axis, y_axis = nxd.axes[-2:]
@@ -902,7 +1045,8 @@ class _NXdataImageView(DataView):
return widget
def axesNames(self, data, info):
- return []
+ # disabled (used by default axis selector widget in Hdf5Viewer)
+ return None
def clear(self):
self.getWidget().clear()
@@ -910,7 +1054,7 @@ class _NXdataImageView(DataView):
def setData(self, data):
data = self.normalizeData(data)
nxd = NXdata(data)
- signal_name = data.attrs["signal"]
+ signal_name = get_attr_as_string(data, "signal")
group_name = data.name
y_axis, x_axis = nxd.axes[-2:]
y_label, x_label = nxd.axes_names[-2:]
@@ -942,7 +1086,8 @@ class _NXdataStackView(DataView):
return widget
def axesNames(self, data, info):
- return []
+ # disabled (used by default axis selector widget in Hdf5Viewer)
+ return None
def clear(self):
self.getWidget().clear()
@@ -950,7 +1095,7 @@ class _NXdataStackView(DataView):
def setData(self, data):
data = self.normalizeData(data)
nxd = NXdata(data)
- signal_name = data.attrs["signal"]
+ signal_name = get_attr_as_string(data, "signal")
group_name = data.name
z_axis, y_axis, x_axis = nxd.axes[-3:]
z_label, y_label, x_label = nxd.axes_names[-3:]
diff --git a/silx/gui/data/Hdf5TableView.py b/silx/gui/data/Hdf5TableView.py
index 5d79907..ba737e3 100644
--- a/silx/gui/data/Hdf5TableView.py
+++ b/silx/gui/data/Hdf5TableView.py
@@ -30,7 +30,7 @@ from __future__ import division
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "07/04/2017"
+__date__ = "29/09/2017"
import functools
import os.path
@@ -40,6 +40,13 @@ import silx.io
from .TextFormatter import TextFormatter
import silx.gui.hdf5
from silx.gui.widgets import HierarchicalTableView
+from ..hdf5.Hdf5Formatter import Hdf5Formatter
+
+try:
+ import h5py
+except ImportError:
+ h5py = None
+
_logger = logging.getLogger(__name__)
@@ -177,6 +184,7 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
self.__obj = None
self.__data = _TableData(columnCount=4)
self.__formatter = None
+ self.__hdf5Formatter = Hdf5Formatter(self)
formatter = TextFormatter(self)
self.setFormatter(formatter)
self.setObject(data)
@@ -207,7 +215,7 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
value = cell.value()
if callable(value):
value = value(self.__obj)
- return str(value)
+ return value
return None
def flags(self, index):
@@ -248,6 +256,22 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
else:
self.reset()
+ def __formatHdf5Type(self, dataset):
+ """Format the HDF5 type"""
+ return self.__hdf5Formatter.humanReadableHdf5Type(dataset)
+
+ def __formatDType(self, dataset):
+ """Format the numpy dtype"""
+ return self.__hdf5Formatter.humanReadableType(dataset, full=True)
+
+ def __formatShape(self, dataset):
+ """Format the shape"""
+ if dataset.shape is None or len(dataset.shape) <= 1:
+ return self.__hdf5Formatter.humanReadableShape(dataset)
+ size = dataset.size
+ shape = self.__hdf5Formatter.humanReadableShape(dataset)
+ return u"%s = %s" % (shape, size)
+
def __initProperties(self):
"""Initialize the list of available properties according to the defined
h5py-like object."""
@@ -270,26 +294,48 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
else:
objectType = obj.__class__.__name__
self.__data.addHeaderRow(headerLabel="HDF5 %s" % objectType)
- self.__data.addHeaderRow(headerLabel="Path info")
- self.__data.addHeaderValueRow("basename", lambda x: os.path.basename(x.name))
- self.__data.addHeaderValueRow("name", lambda x: x.name)
- if silx.io.is_file(obj):
- self.__data.addHeaderValueRow("filename", lambda x: x.filename)
+ SEPARATOR = "::"
+ self.__data.addHeaderRow(headerLabel="Path info")
if isinstance(obj, silx.gui.hdf5.H5Node):
# helpful informations if the object come from an HDF5 tree
- self.__data.addHeaderValueRow("local_basename", lambda x: x.local_basename)
- self.__data.addHeaderValueRow("local_name", lambda x: x.local_name)
- self.__data.addHeaderValueRow("local_filename", lambda x: x.local_file.filename)
+ self.__data.addHeaderValueRow("Basename", lambda x: x.local_basename)
+ self.__data.addHeaderValueRow("Name", lambda x: x.local_name)
+ local = lambda x: x.local_filename + SEPARATOR + x.local_name
+ self.__data.addHeaderValueRow("Local", local)
+ physical = lambda x: x.physical_filename + SEPARATOR + x.physical_name
+ self.__data.addHeaderValueRow("Physical", physical)
+ else:
+ # it's a real H5py object
+ self.__data.addHeaderValueRow("Basename", lambda x: os.path.basename(x.name))
+ self.__data.addHeaderValueRow("Name", lambda x: x.name)
+ self.__data.addHeaderValueRow("File", lambda x: x.file.filename)
+
+ if hasattr(obj, "path"):
+ # That's a link
+ if hasattr(obj, "filename"):
+ link = lambda x: x.filename + SEPARATOR + x.path
+ else:
+ link = lambda x: x.path
+ self.__data.addHeaderValueRow("Link", link)
+ else:
+ if silx.io.is_file(obj):
+ physical = lambda x: x.filename + SEPARATOR + x.name
+ else:
+ physical = lambda x: x.file.filename + SEPARATOR + x.name
+ self.__data.addHeaderValueRow("Physical", physical)
if hasattr(obj, "dtype"):
+
self.__data.addHeaderRow(headerLabel="Data info")
- self.__data.addHeaderValueRow("dtype", lambda x: x.dtype)
+
+ if h5py is not None and hasattr(obj, "id"):
+ # display the HDF5 type
+ self.__data.addHeaderValueRow("HDF5 type", self.__formatHdf5Type)
+ self.__data.addHeaderValueRow("dtype", self.__formatDType)
if hasattr(obj, "shape"):
- self.__data.addHeaderValueRow("shape", lambda x: x.shape)
- if hasattr(obj, "size"):
- self.__data.addHeaderValueRow("size", lambda x: x.size)
+ self.__data.addHeaderValueRow("shape", self.__formatShape)
if hasattr(obj, "chunks") and obj.chunks is not None:
self.__data.addHeaderValueRow("chunks", lambda x: x.chunks)
@@ -354,6 +400,8 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
if formatter is self.__formatter:
return
+ self.__hdf5Formatter.setTextFormatter(formatter)
+
if qt.qVersion() > "4.6":
self.beginResetModel()
diff --git a/silx/gui/data/HexaTableView.py b/silx/gui/data/HexaTableView.py
new file mode 100644
index 0000000..1b2a7e9
--- /dev/null
+++ b/silx/gui/data/HexaTableView.py
@@ -0,0 +1,278 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2017 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""
+This module defines model and widget to display raw data using an
+hexadecimal viewer.
+"""
+from __future__ import division
+
+import numpy
+import collections
+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"]
+__license__ = "MIT"
+__date__ = "27/09/2017"
+
+
+class _VoidConnector(object):
+ """Byte connector to a numpy.void data.
+
+ It uses a cache of 32 x 1KB and a direct read access API from HDF5.
+ """
+
+ def __init__(self, data):
+ self.__cache = collections.OrderedDict()
+ self.__len = data.itemsize
+ self.__data = data
+
+ def __getBuffer(self, bufferId):
+ if bufferId not in self.__cache:
+ pos = bufferId << 10
+ data = self.__data.tobytes()[pos:pos + 1024]
+ self.__cache[bufferId] = data
+ if len(self.__cache) > 32:
+ self.__cache.popitem()
+ else:
+ data = self.__cache[bufferId]
+ return data
+
+ def __getitem__(self, pos):
+ """Returns the value of the byte at the given position.
+
+ :param uint pos: Position of the byte
+ :rtype: int
+ """
+ bufferId = pos >> 10
+ bufferPos = pos & 0b1111111111
+ data = self.__getBuffer(bufferId)
+ value = data[bufferPos]
+ if six.PY2:
+ return ord(value)
+ else:
+ return value
+
+ def __len__(self):
+ """
+ Returns the number of available bytes.
+
+ :rtype: uint
+ """
+ return self.__len
+
+
+class HexaTableModel(qt.QAbstractTableModel):
+ """This data model provides access to a numpy void data.
+
+ Bytes are displayed one by one as a hexadecimal viewer.
+
+ The 16th first columns display bytes as hexadecimal, the last column
+ displays the same data as ASCII.
+
+ :param qt.QObject parent: Parent object
+ :param data: A numpy array or a h5py dataset
+ """
+ def __init__(self, parent=None, data=None):
+ qt.QAbstractTableModel.__init__(self, parent)
+
+ self.__data = None
+ self.__connector = None
+ self.setArrayData(data)
+
+ if hasattr(qt.QFontDatabase, "systemFont"):
+ self.__font = qt.QFontDatabase.systemFont(qt.QFontDatabase.FixedFont)
+ else:
+ self.__font = qt.QFont("Monospace")
+ self.__font.setStyleHint(qt.QFont.TypeWriter)
+ self.__palette = qt.QPalette()
+
+ def rowCount(self, parent_idx=None):
+ """Returns number of rows to be displayed in table"""
+ if self.__connector is None:
+ return 0
+ return ((len(self.__connector) - 1) >> 4) + 1
+
+ def columnCount(self, parent_idx=None):
+ """Returns number of columns to be displayed in table"""
+ return 0x10 + 1
+
+ def data(self, index, role=qt.Qt.DisplayRole):
+ """QAbstractTableModel method to access data values
+ in the format ready to be displayed"""
+ if not index.isValid():
+ return None
+
+ if self.__connector is None:
+ return None
+
+ row = index.row()
+ column = index.column()
+
+ if role == qt.Qt.DisplayRole:
+ if column == 0x10:
+ start = (row << 4)
+ text = ""
+ for i in range(0x10):
+ pos = start + i
+ if pos >= len(self.__connector):
+ break
+ value = self.__connector[pos]
+ if value > 0x20 and value < 0x7F:
+ text += chr(value)
+ else:
+ text += "."
+ return text
+ else:
+ pos = (row << 4) + column
+ if pos < len(self.__connector):
+ value = self.__connector[pos]
+ return "%02X" % value
+ else:
+ return ""
+ elif role == qt.Qt.FontRole:
+ return self.__font
+
+ elif role == qt.Qt.BackgroundColorRole:
+ pos = (row << 4) + column
+ if column != 0x10 and pos >= len(self.__connector):
+ return self.__palette.color(qt.QPalette.Disabled, qt.QPalette.Background)
+ else:
+ return None
+
+ return None
+
+ def headerData(self, section, orientation, role=qt.Qt.DisplayRole):
+ """Returns the 0-based row or column index, for display in the
+ horizontal and vertical headers"""
+ if section == -1:
+ # PyQt4 send -1 when there is columns but no rows
+ return None
+
+ if role == qt.Qt.DisplayRole:
+ if orientation == qt.Qt.Vertical:
+ return "%02X" % (section << 4)
+ if orientation == qt.Qt.Horizontal:
+ if section == 0x10:
+ return "ASCII"
+ else:
+ return "%02X" % section
+ elif role == qt.Qt.FontRole:
+ return self.__font
+ elif role == qt.Qt.TextAlignmentRole:
+ if orientation == qt.Qt.Vertical:
+ return qt.Qt.AlignRight
+ if orientation == qt.Qt.Horizontal:
+ if section == 0x10:
+ return qt.Qt.AlignLeft
+ else:
+ return qt.Qt.AlignCenter
+ return None
+
+ def flags(self, index):
+ """QAbstractTableModel method to inform the view whether data
+ is editable or not.
+ """
+ row = index.row()
+ column = index.column()
+ pos = (row << 4) + column
+ if column != 0x10 and pos >= len(self.__connector):
+ return qt.Qt.NoItemFlags
+ return qt.QAbstractTableModel.flags(self, index)
+
+ def setArrayData(self, data):
+ """Set the data array.
+
+ :param data: A numpy object or a dataset.
+ """
+ if qt.qVersion() > "4.6":
+ self.beginResetModel()
+
+ self.__connector = None
+ self.__data = data
+ if self.__data is not None:
+ if silx.io.utils.is_dataset(self.__data):
+ data = data[()]
+ elif isinstance(self.__data, numpy.ndarray):
+ data = data[()]
+ self.__connector = _VoidConnector(data)
+
+ if qt.qVersion() > "4.6":
+ self.endResetModel()
+ else:
+ self.reset()
+
+ def arrayData(self):
+ """Returns the internal data.
+
+ :rtype: numpy.ndarray of h5py.Dataset
+ """
+ return self.__data
+
+
+class HexaTableView(qt.QTableView):
+ """TableView using HexaTableModel as default model.
+
+ It customs the column size to provide a better layout.
+ """
+ def __init__(self, parent=None):
+ """
+ Constructor
+
+ :param qt.QWidget parent: parent QWidget
+ """
+ qt.QTableView.__init__(self, parent)
+
+ model = HexaTableModel(self)
+ self.setModel(model)
+ self._copyAction = CopySelectedCellsAction(self)
+ self.addAction(self._copyAction)
+
+ def copy(self):
+ self._copyAction.trigger()
+
+ def setArrayData(self, data):
+ """Set the data array.
+
+ :param data: A numpy object or a dataset.
+ """
+ self.model().setArrayData(data)
+ self.__fixHeader()
+
+ def __fixHeader(self):
+ """Update the view according to the state of the auto-resize"""
+ header = self.horizontalHeader()
+ if qt.qVersion() < "5.0":
+ setResizeMode = header.setResizeMode
+ else:
+ setResizeMode = header.setSectionResizeMode
+
+ header.setDefaultSectionSize(30)
+ header.setStretchLastSection(True)
+ for i in range(0x10):
+ setResizeMode(i, qt.QHeaderView.Fixed)
+ setResizeMode(0x10, qt.QHeaderView.Stretch)
diff --git a/silx/gui/data/NXdataWidgets.py b/silx/gui/data/NXdataWidgets.py
index 343c7f9..b820380 100644
--- a/silx/gui/data/NXdataWidgets.py
+++ b/silx/gui/data/NXdataWidgets.py
@@ -26,7 +26,7 @@
"""
__authors__ = ["P. Knobel"]
__license__ = "MIT"
-__date__ = "20/03/2017"
+__date__ = "27/06/2017"
import numpy
@@ -135,8 +135,8 @@ class ArrayCurvePlot(qt.QWidget):
self.selectorDock.show()
self._plot.setGraphTitle(title or "")
- self._plot.setGraphXLabel(self.__axis_name or "X")
- self._plot.setGraphYLabel(self.__signal_name or "Y")
+ self._plot.getXAxis().setLabel(self.__axis_name or "X")
+ self._plot.getYAxis().setLabel(self.__signal_name or "Y")
self._updateCurve()
if not self.__selector_is_connected:
@@ -188,8 +188,8 @@ class ArrayCurvePlot(qt.QWidget):
xerror=self.__axis_errors,
yerror=y_errors)
self._plot.resetZoom()
- self._plot.setGraphXLabel(self.__axis_name)
- self._plot.setGraphYLabel(self.__signal_name)
+ self._plot.getXAxis().setLabel(self.__axis_name)
+ self._plot.getYAxis().setLabel(self.__signal_name)
def clear(self):
self._plot.clear()
@@ -289,8 +289,8 @@ class ArrayImagePlot(qt.QWidget):
self.selectorDock.show()
self._plot.setGraphTitle(title or "")
- self._plot.setGraphXLabel(self.__x_axis_name or "X")
- self._plot.setGraphYLabel(self.__y_axis_name or "Y")
+ self._plot.getXAxis().setLabel(self.__x_axis_name or "X")
+ self._plot.getYAxis().setLabel(self.__y_axis_name or "Y")
self._updateImage()
@@ -352,8 +352,8 @@ class ArrayImagePlot(qt.QWidget):
numpy.ravel(scattery),
numpy.ravel(img),
legend=legend)
- self._plot.setGraphXLabel(self.__x_axis_name)
- self._plot.setGraphYLabel(self.__y_axis_name)
+ self._plot.getXAxis().setLabel(self.__x_axis_name)
+ self._plot.getYAxis().setLabel(self.__y_axis_name)
self._plot.resetZoom()
def clear(self):
@@ -450,8 +450,8 @@ class ArrayStackPlot(qt.QWidget):
self._stack_view.setGraphTitle(title or "")
# by default, the z axis is the image position (dimension not plotted)
- self._stack_view.setGraphXLabel(self.__x_axis_name or "X")
- self._stack_view.setGraphYLabel(self.__y_axis_name or "Y")
+ self._stack_view.getPlot().getXAxis().setLabel(self.__x_axis_name or "X")
+ self._stack_view.getPlot().getYAxis().setLabel(self.__y_axis_name or "Y")
self._updateStack()
diff --git a/silx/gui/data/RecordTableView.py b/silx/gui/data/RecordTableView.py
index ce6a178..54881b7 100644
--- a/silx/gui/data/RecordTableView.py
+++ b/silx/gui/data/RecordTableView.py
@@ -37,7 +37,7 @@ from silx.gui.widgets.TableWidget import CopySelectedCellsAction
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "27/01/2017"
+__date__ = "02/10/2017"
class _MultiLineItem(qt.QItemDelegate):
@@ -206,9 +206,9 @@ class RecordTableModel(qt.QAbstractTableModel):
data = data[key[1]]
if role == qt.Qt.DisplayRole:
- return self.__formatter.toString(data)
+ return self.__formatter.toString(data, dtype=self.__data.dtype)
elif role == qt.Qt.EditRole:
- return self.__editFormatter.toString(data)
+ return self.__editFormatter.toString(data, dtype=self.__data.dtype)
return None
def headerData(self, section, orientation, role=qt.Qt.DisplayRole):
@@ -270,11 +270,11 @@ class RecordTableModel(qt.QAbstractTableModel):
else:
self.__is_array = False
-
self.__fields = []
if data is not None:
if data.dtype.fields is not None:
- for name, (dtype, _index) in data.dtype.fields.items():
+ fields = sorted(data.dtype.fields.items(), key=lambda e: e[1][1])
+ for name, (dtype, _index) in fields:
if dtype.shape != tuple():
keys = itertools.product(*[range(x) for x in dtype.shape])
for key in keys:
diff --git a/silx/gui/data/TextFormatter.py b/silx/gui/data/TextFormatter.py
index f074de5..37e1f48 100644
--- a/silx/gui/data/TextFormatter.py
+++ b/silx/gui/data/TextFormatter.py
@@ -27,14 +27,18 @@ data module to format data as text in the same way."""
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "26/04/2017"
+__date__ = "27/09/2017"
import numpy
import numbers
-import binascii
from silx.third_party import six
from silx.gui import qt
+try:
+ import h5py
+except ImportError:
+ h5py = None
+
class TextFormatter(qt.QObject):
"""Formatter to convert data to string.
@@ -73,11 +77,13 @@ class TextFormatter(qt.QObject):
self.__floatFormat = formatter.floatFormat()
self.__useQuoteForText = formatter.useQuoteForText()
self.__imaginaryUnit = formatter.imaginaryUnit()
+ self.__enumFormat = formatter.enumFormat()
else:
self.__integerFormat = "%d"
self.__floatFormat = "%g"
self.__useQuoteForText = True
self.__imaginaryUnit = u"j"
+ self.__enumFormat = u"%(name)s(%(value)d)"
def integerFormat(self):
"""Returns the format string controlling how the integer data
@@ -162,40 +168,151 @@ class TextFormatter(qt.QObject):
self.__imaginaryUnit = imaginaryUnit
self.formatChanged.emit()
- def toString(self, data):
+ def setEnumFormat(self, value):
+ """Set format string controlling how the enum data are
+ formated by this object.
+
+ :param str value: Format string (e.g. "%(name)s(%(value)d)").
+ This is the C-style format string used by python when formatting
+ strings with the modulus operator.
+ """
+ if self.__enumFormat == value:
+ return
+ self.__enumFormat = value
+ self.formatChanged.emit()
+
+ def enumFormat(self):
+ """Returns the format string controlling how the enum data
+ are formated by this object.
+
+ This is the C-style format string used by python when formatting
+ strings with the modulus operator.
+
+ :rtype: str
+ """
+ return self.__enumFormat
+
+ def __formatText(self, text):
+ if self.__useQuoteForText:
+ text = "\"%s\"" % text.replace("\\", "\\\\").replace("\"", "\\\"")
+ return text
+
+ def __formatBinary(self, data):
+ if isinstance(data, numpy.void):
+ if six.PY2:
+ data = [ord(d) for d in data.item()]
+ else:
+ data = data.item().astype(numpy.uint8)
+ else:
+ data = [ord(d) for d in data]
+ data = ["\\x%02X" % d for d in data]
+ if self.__useQuoteForText:
+ return "b\"%s\"" % "".join(data)
+ else:
+ return "".join(data)
+
+ def __formatSafeAscii(self, data):
+ if six.PY2:
+ data = [ord(d) for d in data]
+ data = [chr(d) if (d > 0x20 and d < 0x7F) else "\\x%02X" % d for d in data]
+ if self.__useQuoteForText:
+ data = [c if c != '"' else "\\" + c for c in data]
+ return "b\"%s\"" % "".join(data)
+ else:
+ return "".join(data)
+
+ def __formatH5pyObject(self, data, dtype):
+ # That's an HDF5 object
+ ref = h5py.check_dtype(ref=dtype)
+ if ref is not None:
+ if bool(data):
+ return "REF"
+ else:
+ return "NULL_REF"
+ vlen = h5py.check_dtype(vlen=dtype)
+ if vlen is not None:
+ if vlen == six.text_type:
+ # HDF5 UTF8
+ return self.__formatText(data)
+ elif vlen == six.binary_type:
+ # HDF5 ASCII
+ try:
+ text = "%s" % data.decode("ascii")
+ return self.__formatText(text)
+ except UnicodeDecodeError:
+ return self.__formatSafeAscii(data)
+ return None
+
+ def toString(self, data, dtype=None):
"""Format a data into a string using formatter options
:param object data: Data to render
+ :param dtype: enforce a dtype (mostly used to remember the h5py dtype,
+ special h5py dtypes are not propagated from array to items)
:rtype: str
"""
if isinstance(data, tuple):
text = [self.toString(d) for d in data]
return "(" + " ".join(text) + ")"
- elif isinstance(data, (list, numpy.ndarray)):
+ elif isinstance(data, list):
text = [self.toString(d) for d in data]
return "[" + " ".join(text) + "]"
+ elif isinstance(data, (numpy.ndarray)):
+ if dtype is None:
+ dtype = data.dtype
+ if data.shape == ():
+ # it is a scaler
+ return self.toString(data[()], dtype)
+ else:
+ text = [self.toString(d, dtype) for d in data]
+ return "[" + " ".join(text) + "]"
elif isinstance(data, numpy.void):
- dtype = data.dtype
+ if dtype is None:
+ dtype = data.dtype
if data.dtype.fields is not None:
- text = [self.toString(data[f]) for f in dtype.fields]
+ text = [self.toString(data[f], dtype) for f in dtype.fields]
return "(" + " ".join(text) + ")"
- return "0x" + binascii.hexlify(data).decode("ascii")
- elif isinstance(data, (numpy.string_, numpy.object_, bytes)):
- # This have to be done before checking python string inheritance
+ return self.__formatBinary(data)
+ elif isinstance(data, (numpy.unicode_, six.text_type)):
+ return self.__formatText(data)
+ elif isinstance(data, (numpy.string_, six.binary_type)):
+ if dtype is not None:
+ # Maybe a sub item from HDF5
+ if dtype.kind == 'S':
+ try:
+ text = "%s" % data.decode("ascii")
+ return self.__formatText(text)
+ except UnicodeDecodeError:
+ return self.__formatSafeAscii(data)
+ elif dtype.kind == 'O':
+ if h5py is not None:
+ text = self.__formatH5pyObject(data, dtype)
+ if text is not None:
+ return text
try:
+ # Try ascii/utf-8
text = "%s" % data.decode("utf-8")
- if self.__useQuoteForText:
- text = "\"%s\"" % text.replace("\"", "\\\"")
- return text
+ return self.__formatText(text)
except UnicodeDecodeError:
pass
- return "0x" + binascii.hexlify(data).decode("ascii")
+ return self.__formatBinary(data)
elif isinstance(data, six.string_types):
text = "%s" % data
- if self.__useQuoteForText:
- text = "\"%s\"" % text.replace("\"", "\\\"")
- return text
- elif isinstance(data, (numpy.integer, numbers.Integral)):
+ return self.__formatText(text)
+ 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
+ return self.__integerFormat % data
+ elif isinstance(data, (numbers.Integral)):
return self.__integerFormat % data
elif isinstance(data, (numbers.Real, numpy.floating)):
# It have to be done before complex checking
@@ -219,4 +336,21 @@ class TextFormatter(qt.QObject):
template = self.__floatFormat
params = (data.real)
return template % params
+ elif h5py is not None and 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):
+ 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
+ # That's a numpy object
+ return str(data)
return str(data)
diff --git a/silx/gui/data/test/test_dataviewer.py b/silx/gui/data/test/test_dataviewer.py
index 5a0de0b..dd3114a 100644
--- a/silx/gui/data/test/test_dataviewer.py
+++ b/silx/gui/data/test/test_dataviewer.py
@@ -24,7 +24,7 @@
# ###########################################################################*/
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "10/04/2017"
+__date__ = "22/08/2017"
import os
import tempfile
@@ -42,8 +42,6 @@ from silx.gui.data.DataViewerFrame import DataViewerFrame
from silx.gui.test.utils import SignalListener
from silx.gui.test.utils import TestCaseQt
-from silx.gui.hdf5.test import _mock
-
try:
import h5py
except ImportError:
@@ -111,6 +109,24 @@ class AbstractDataViewerTests(TestCaseQt):
self.assertEqual(DataViewer.RAW_MODE, widget.displayMode())
self.assertIn(DataViewer.PLOT2D_MODE, availableModes)
+ def test_plot_2d_bool(self):
+ data = numpy.zeros((10, 10), dtype=numpy.bool)
+ data[::2, ::2] = True
+ widget = self.create_widget()
+ widget.setData(data)
+ availableModes = set([v.modeId() for v in widget.currentAvailableViews()])
+ self.assertEqual(DataViewer.RAW_MODE, widget.displayMode())
+ self.assertIn(DataViewer.PLOT2D_MODE, availableModes)
+
+ def test_plot_2d_complex_data(self):
+ data = numpy.arange(3 ** 2, dtype=numpy.complex)
+ data.shape = [3] * 2
+ widget = self.create_widget()
+ widget.setData(data)
+ availableModes = set([v.modeId() for v in widget.currentAvailableViews()])
+ self.assertEqual(DataViewer.RAW_MODE, widget.displayMode())
+ self.assertIn(DataViewer.PLOT2D_MODE, availableModes)
+
def test_plot_3d_data(self):
data = numpy.arange(3 ** 3)
data.shape = [3] * 3
@@ -212,6 +228,7 @@ class AbstractDataViewerTests(TestCaseQt):
self.assertTrue(view not in widget.availableViews())
self.assertTrue(view not in widget.currentAvailableViews())
+
class TestDataViewer(AbstractDataViewerTests):
def create_widget(self):
return DataViewer()
@@ -225,11 +242,10 @@ class TestDataViewerFrame(AbstractDataViewerTests):
class TestDataView(TestCaseQt):
def createComplexData(self):
- line = [1, 2j, 3+3j, 4]
+ line = [1, 2j, 3 + 3j, 4]
image = [line, line, line, line]
cube = [image, image, image, image]
- data = numpy.array(cube,
- dtype=numpy.complex)
+ data = numpy.array(cube, dtype=numpy.complex)
return data
def createDataViewWithData(self, dataViewClass, data):
diff --git a/silx/gui/data/test/test_textformatter.py b/silx/gui/data/test/test_textformatter.py
index f21e033..2a7a66b 100644
--- a/silx/gui/data/test/test_textformatter.py
+++ b/silx/gui/data/test/test_textformatter.py
@@ -24,13 +24,22 @@
# ###########################################################################*/
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "24/01/2017"
+__date__ = "27/09/2017"
import unittest
+import shutil
+import tempfile
+import numpy
from silx.gui.test.utils import TestCaseQt
from silx.gui.test.utils import SignalListener
from ..TextFormatter import TextFormatter
+from silx.third_party import six
+
+try:
+ import h5py
+except ImportError:
+ h5py = None
class TestTextFormatter(TestCaseQt):
@@ -83,10 +92,108 @@ class TestTextFormatter(TestCaseQt):
self.assertEquals(result, '"toto"')
+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")
+ cls.formatter = TextFormatter()
+
+ @classmethod
+ def tearDownClass(cls):
+ super(TestTextFormatterWithH5py, cls).tearDownClass()
+ cls.h5File.close()
+ cls.h5File = None
+ shutil.rmtree(cls.tmpDirectory)
+
+ def create_dataset(self, data, dtype=None):
+ testName = "%s" % self.id()
+ dataset = self.h5File.create_dataset(testName, data=data, dtype=dtype)
+ return dataset
+
+ def testAscii(self):
+ d = self.create_dataset(data=b"abc")
+ result = self.formatter.toString(d[()], dtype=d.dtype)
+ self.assertEquals(result, '"abc"')
+
+ def testUnicode(self):
+ d = self.create_dataset(data=u"i\u2661cookies")
+ result = self.formatter.toString(d[()], dtype=d.dtype)
+ self.assertEquals(len(result), 11)
+ self.assertEquals(result, u'"i\u2661cookies"')
+
+ def testBadAscii(self):
+ d = self.create_dataset(data=b"\xF0\x9F\x92\x94")
+ result = self.formatter.toString(d[()], dtype=d.dtype)
+ self.assertEquals(result, 'b"\\xF0\\x9F\\x92\\x94"')
+
+ def testVoid(self):
+ d = self.create_dataset(data=numpy.void(b"abc\xF0"))
+ result = self.formatter.toString(d[()], dtype=d.dtype)
+ self.assertEquals(result, 'b"\\x61\\x62\\x63\\xF0"')
+
+ def testEnum(self):
+ dtype = h5py.special_dtype(enum=('i', {"RED": 0, "GREEN": 1, "BLUE": 42}))
+ d = numpy.array(42, dtype=dtype)
+ d = self.create_dataset(data=d)
+ result = self.formatter.toString(d[()], dtype=d.dtype)
+ self.assertEquals(result, 'BLUE(42)')
+
+ def testRef(self):
+ dtype = h5py.special_dtype(ref=h5py.Reference)
+ d = numpy.array(self.h5File.ref, dtype=dtype)
+ d = self.create_dataset(data=d)
+ result = self.formatter.toString(d[()], dtype=d.dtype)
+ self.assertEquals(result, 'REF')
+
+ def testArrayAscii(self):
+ d = self.create_dataset(data=[b"abc"])
+ result = self.formatter.toString(d[()], dtype=d.dtype)
+ self.assertEquals(result, '["abc"]')
+
+ def testArrayUnicode(self):
+ dtype = h5py.special_dtype(vlen=six.text_type)
+ d = numpy.array([u"i\u2661cookies"], dtype=dtype)
+ d = self.create_dataset(data=d)
+ result = self.formatter.toString(d[()], dtype=d.dtype)
+ self.assertEquals(len(result), 13)
+ self.assertEquals(result, u'["i\u2661cookies"]')
+
+ def testArrayBadAscii(self):
+ d = self.create_dataset(data=[b"\xF0\x9F\x92\x94"])
+ result = self.formatter.toString(d[()], dtype=d.dtype)
+ self.assertEquals(result, '[b"\\xF0\\x9F\\x92\\x94"]')
+
+ def testArrayVoid(self):
+ d = self.create_dataset(data=numpy.void([b"abc\xF0"]))
+ result = self.formatter.toString(d[()], dtype=d.dtype)
+ self.assertEquals(result, '[b"\\x61\\x62\\x63\\xF0"]')
+
+ def testArrayEnum(self):
+ dtype = h5py.special_dtype(enum=('i', {"RED": 0, "GREEN": 1, "BLUE": 42}))
+ d = numpy.array([42, 1, 100], dtype=dtype)
+ d = self.create_dataset(data=d)
+ result = self.formatter.toString(d[()], dtype=d.dtype)
+ self.assertEquals(result, '[BLUE(42) GREEN(1) 100]')
+
+ def testArrayRef(self):
+ dtype = h5py.special_dtype(ref=h5py.Reference)
+ d = numpy.array([self.h5File.ref, None], dtype=dtype)
+ d = self.create_dataset(data=d)
+ result = self.formatter.toString(d[()], dtype=d.dtype)
+ self.assertEquals(result, '[REF NULL_REF]')
+
+
def suite():
+ loadTests = unittest.defaultTestLoader.loadTestsFromTestCase
test_suite = unittest.TestSuite()
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestTextFormatter))
+ test_suite.addTest(loadTests(TestTextFormatter))
+ test_suite.addTest(loadTests(TestTextFormatterWithH5py))
return test_suite
diff --git a/silx/gui/fit/BackgroundWidget.py b/silx/gui/fit/BackgroundWidget.py
index 577a8c7..2171e87 100644
--- a/silx/gui/fit/BackgroundWidget.py
+++ b/silx/gui/fit/BackgroundWidget.py
@@ -26,7 +26,11 @@
# #########################################################################*/
"""This module provides a background configuration widget
:class:`BackgroundWidget` and a corresponding dialog window
-:class:`BackgroundDialog`."""
+:class:`BackgroundDialog`.
+
+.. image:: img/BackgroundDialog.png
+ :height: 300px
+"""
import sys
import numpy
from silx.gui import qt
@@ -35,7 +39,7 @@ from silx.math.fit import filters
__authors__ = ["V.A. Sole", "P. Knobel"]
__license__ = "MIT"
-__date__ = "24/01/2017"
+__date__ = "28/06/2017"
class HorizontalSpacer(qt.QWidget):
@@ -262,7 +266,7 @@ class BackgroundParamWidget(qt.QWidget):
class BackgroundWidget(qt.QWidget):
- """Background configuration widget, with a :class:`PlotWindow`.
+ """Background configuration widget, with a plot to preview the results.
Strip and snip filters parameters can be adjusted using input widgets,
and the computed backgrounds are plotted next to the original data to
@@ -400,7 +404,7 @@ class BackgroundWidget(qt.QWidget):
legend='SNIP Background',
resetzoom=False)
if self._xmin is not None and self._xmax is not None:
- self.graphWidget.setGraphXLimits(xmin=self._xmin, xmax=self._xmax)
+ self.graphWidget.getXAxis().setLimits(self._xmin, self._xmax)
class BackgroundDialog(qt.QDialog):
@@ -467,11 +471,11 @@ class BackgroundDialog(qt.QDialog):
return self.parametersWidget.getParameters()
def setParameters(self, ddict):
- """See :meth:`BackgroundWidget.setParameters`"""
+ """See :meth:`BackgroundWidget.setPrintGeometry`"""
return self.parametersWidget.setParameters(ddict)
def setDefault(self, ddict):
- """Alias for :meth:`setParameters`"""
+ """Alias for :meth:`setPrintGeometry`"""
return self.setParameters(ddict)
diff --git a/silx/gui/fit/FitConfig.py b/silx/gui/fit/FitConfig.py
index 70b6fbe..04e411b 100644
--- a/silx/gui/fit/FitConfig.py
+++ b/silx/gui/fit/FitConfig.py
@@ -307,7 +307,7 @@ class SearchPage(qt.QWidget):
self.yScalingEntry.setToolTip(
"Data values will be multiplied by this value prior to peak" +
" search")
- self.yScalingEntry.setValidator(qt.QDoubleValidator())
+ self.yScalingEntry.setValidator(qt.QDoubleValidator(self))
layout3.addWidget(self.yScalingEntry)
# ----------------------------------------------------
@@ -324,7 +324,7 @@ class SearchPage(qt.QWidget):
"Peak search sensitivity threshold, expressed as a multiple " +
"of the standard deviation of the noise.\nMinimum value is 1 " +
"(to be detected, peak must be higher than the estimated noise)")
- sensivalidator = qt.QDoubleValidator()
+ sensivalidator = qt.QDoubleValidator(self)
sensivalidator.setBottom(1.0)
self.sensitivityEntry.setValidator(sensivalidator)
layout4.addWidget(self.sensitivityEntry)
@@ -418,7 +418,7 @@ class BackgroundPage(qt.QGroupBox):
"Factor used by the strip algorithm to decide whether a sample" +
"value should be stripped.\nThe value must be higher than the " +
"average of the 2 samples at +- w times this factor.\n")
- self.thresholdFactorEntry.setValidator(qt.QDoubleValidator())
+ self.thresholdFactorEntry.setValidator(qt.QDoubleValidator(self))
layout.addWidget(self.thresholdFactorEntry, 2, 1)
self.smoothStripGB = qt.QGroupBox("Apply smoothing prior to strip", self)
diff --git a/silx/gui/fit/FitWidget.py b/silx/gui/fit/FitWidget.py
index a5c3cfd..7012b63 100644
--- a/silx/gui/fit/FitWidget.py
+++ b/silx/gui/fit/FitWidget.py
@@ -87,6 +87,8 @@ class FitWidget(qt.QWidget):
run the estimation, set constraints on parameters and run the actual fit.
The results are displayed in a table.
+
+ .. image:: img/FitWidget.png
"""
sigFitWidgetSignal = qt.Signal(object)
"""This signal is emitted by the estimation and fit methods.
diff --git a/silx/gui/hdf5/Hdf5Formatter.py b/silx/gui/hdf5/Hdf5Formatter.py
new file mode 100644
index 0000000..3a4c1c1
--- /dev/null
+++ b/silx/gui/hdf5/Hdf5Formatter.py
@@ -0,0 +1,229 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2017 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This package provides a class sharred by widgets to format HDF5 data as
+text."""
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "27/09/2017"
+
+import numpy
+from silx.third_party import six
+from silx.gui import qt
+from silx.gui.data.TextFormatter import TextFormatter
+
+try:
+ import h5py
+except ImportError:
+ h5py = None
+
+
+class Hdf5Formatter(qt.QObject):
+ """Formatter to convert HDF5 data to string.
+ """
+
+ formatChanged = qt.Signal()
+ """Emitted when properties of the formatter change."""
+
+ def __init__(self, parent=None, textFormatter=None):
+ """
+ Constructor
+
+ :param qt.QObject parent: Owner of the object
+ :param TextFormatter formatter: Text formatter
+ """
+ qt.QObject.__init__(self, parent)
+ if textFormatter is not None:
+ self.__formatter = textFormatter
+ else:
+ self.__formatter = TextFormatter(self)
+ self.__formatter.formatChanged.connect(self.__formatChanged)
+
+ def textFormatter(self):
+ """Returns the used text formatter
+
+ :rtype: TextFormatter
+ """
+ return self.__formatter
+
+ def setTextFormatter(self, textFormatter):
+ """Set the text formatter to be used
+
+ :param TextFormatter textFormatter: The text formatter to use
+ """
+ if textFormatter is None:
+ raise ValueError("Formatter expected but None found")
+ if self.__formatter is textFormatter:
+ return
+ self.__formatter.formatChanged.disconnect(self.__formatChanged)
+ self.__formatter = textFormatter
+ self.__formatter.formatChanged.connect(self.__formatChanged)
+ self.__formatChanged()
+
+ def __formatChanged(self):
+ self.formatChanged.emit()
+
+ def humanReadableShape(self, dataset):
+ if dataset.shape is None:
+ return "none"
+ if dataset.shape == tuple():
+ return "scalar"
+ shape = [str(i) for i in dataset.shape]
+ text = u" \u00D7 ".join(shape)
+ return text
+
+ def humanReadableValue(self, dataset):
+ if dataset.shape is None:
+ return "No data"
+
+ dtype = dataset.dtype
+ if dataset.dtype.type == numpy.void:
+ if dtype.fields is None:
+ return "Raw data"
+
+ if dataset.shape == tuple():
+ numpy_object = dataset[()]
+ text = self.__formatter.toString(numpy_object, dtype=dataset.dtype)
+ else:
+ if dataset.size < 5 and dataset.compression is None:
+ numpy_object = dataset[0:5]
+ text = self.__formatter.toString(numpy_object, dtype=dataset.dtype)
+ else:
+ dimension = len(dataset.shape)
+ if dataset.compression is not None:
+ text = "Compressed %dD data" % dimension
+ else:
+ text = "%dD data" % dimension
+ return text
+
+ def humanReadableType(self, dataset, full=False):
+ dtype = dataset.dtype
+ return self.humanReadableDType(dtype, full)
+
+ def humanReadableDType(self, dtype, full=False):
+ if dtype == six.binary_type or numpy.issubdtype(dtype, numpy.string_):
+ text = "string"
+ if full:
+ text = "ASCII " + text
+ return text
+ elif dtype == six.text_type or numpy.issubdtype(dtype, numpy.unicode_):
+ text = "string"
+ if full:
+ text = "UTF-8 " + text
+ return text
+ elif dtype.type == numpy.object_:
+ ref = h5py.check_dtype(ref=dtype)
+ if ref is not None:
+ return "reference"
+ vlen = h5py.check_dtype(vlen=dtype)
+ if vlen is not None:
+ text = self.humanReadableDType(vlen, full=full)
+ if full:
+ text = "variable-length " + text
+ return text
+ return "object"
+ elif dtype.type == numpy.bool_:
+ return "bool"
+ elif dtype.type == numpy.void:
+ if dtype.fields is None:
+ return "opaque"
+ else:
+ if not full:
+ return "compound"
+ else:
+ compound = [d[0] for d in dtype.fields.values()]
+ 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"
+
+ text = str(dtype.newbyteorder('N'))
+ if full:
+ if dtype.byteorder == "<":
+ text = "Little-endian " + text
+ elif dtype.byteorder == ">":
+ text = "Big-endian " + text
+ elif dtype.byteorder == "=":
+ text = "Native " + text
+
+ dtype = dtype.newbyteorder('N')
+ return text
+
+ def humanReadableHdf5Type(self, dataset):
+ """Format the internal HDF5 type as a string"""
+ t = dataset.id.get_type()
+ class_ = t.get_class()
+ if class_ == h5py.h5t.NO_CLASS:
+ return "NO_CLASS"
+ elif class_ == h5py.h5t.INTEGER:
+ return "INTEGER"
+ elif class_ == h5py.h5t.FLOAT:
+ return "FLOAT"
+ elif class_ == h5py.h5t.TIME:
+ return "TIME"
+ elif class_ == h5py.h5t.STRING:
+ charset = t.get_cset()
+ strpad = t.get_strpad()
+ text = ""
+
+ if strpad == h5py.h5t.STR_NULLTERM:
+ text += "NULLTERM"
+ elif strpad == h5py.h5t.STR_NULLPAD:
+ text += "NULLPAD"
+ elif strpad == h5py.h5t.STR_SPACEPAD:
+ text += "SPACEPAD"
+ else:
+ text += "UNKNOWN_STRPAD"
+
+ if t.is_variable_str():
+ text += " VARIABLE"
+
+ if charset == h5py.h5t.CSET_ASCII:
+ text += " ASCII"
+ elif charset == h5py.h5t.CSET_UTF8:
+ text += " UTF8"
+ else:
+ text += " UNKNOWN_CSET"
+
+ return text + " STRING"
+ elif class_ == h5py.h5t.BITFIELD:
+ return "BITFIELD"
+ elif class_ == h5py.h5t.OPAQUE:
+ return "OPAQUE"
+ elif class_ == h5py.h5t.COMPOUND:
+ return "COMPOUND"
+ elif class_ == h5py.h5t.REFERENCE:
+ return "REFERENCE"
+ elif class_ == h5py.h5t.ENUM:
+ return "ENUM"
+ elif class_ == h5py.h5t.VLEN:
+ return "VLEN"
+ elif class_ == h5py.h5t.ARRAY:
+ return "ARRAY"
+ else:
+ return "UNKNOWN_CLASS"
diff --git a/silx/gui/hdf5/Hdf5HeaderView.py b/silx/gui/hdf5/Hdf5HeaderView.py
index 5912230..7baa6e0 100644
--- a/silx/gui/hdf5/Hdf5HeaderView.py
+++ b/silx/gui/hdf5/Hdf5HeaderView.py
@@ -25,10 +25,11 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "08/11/2016"
+__date__ = "16/06/2017"
from .. import qt
+from .Hdf5TreeModel import Hdf5TreeModel
QTVERSION = qt.qVersion()
@@ -83,19 +84,21 @@ class Hdf5HeaderView(qt.QHeaderView):
setResizeMode = self.setSectionResizeMode
if self.__auto_resize:
- setResizeMode(0, qt.QHeaderView.ResizeToContents)
- setResizeMode(1, qt.QHeaderView.ResizeToContents)
- setResizeMode(2, qt.QHeaderView.ResizeToContents)
- setResizeMode(3, qt.QHeaderView.Interactive)
- setResizeMode(4, qt.QHeaderView.Interactive)
- setResizeMode(5, qt.QHeaderView.ResizeToContents)
+ setResizeMode(Hdf5TreeModel.NAME_COLUMN, qt.QHeaderView.ResizeToContents)
+ setResizeMode(Hdf5TreeModel.TYPE_COLUMN, qt.QHeaderView.ResizeToContents)
+ setResizeMode(Hdf5TreeModel.SHAPE_COLUMN, qt.QHeaderView.ResizeToContents)
+ setResizeMode(Hdf5TreeModel.VALUE_COLUMN, qt.QHeaderView.Interactive)
+ setResizeMode(Hdf5TreeModel.DESCRIPTION_COLUMN, qt.QHeaderView.Interactive)
+ setResizeMode(Hdf5TreeModel.NODE_COLUMN, qt.QHeaderView.ResizeToContents)
+ setResizeMode(Hdf5TreeModel.LINK_COLUMN, qt.QHeaderView.ResizeToContents)
else:
- setResizeMode(0, qt.QHeaderView.Interactive)
- setResizeMode(1, qt.QHeaderView.Interactive)
- setResizeMode(2, qt.QHeaderView.Interactive)
- setResizeMode(3, qt.QHeaderView.Interactive)
- setResizeMode(4, qt.QHeaderView.Interactive)
- setResizeMode(5, qt.QHeaderView.Interactive)
+ setResizeMode(Hdf5TreeModel.NAME_COLUMN, qt.QHeaderView.Interactive)
+ setResizeMode(Hdf5TreeModel.TYPE_COLUMN, qt.QHeaderView.Interactive)
+ setResizeMode(Hdf5TreeModel.SHAPE_COLUMN, qt.QHeaderView.Interactive)
+ setResizeMode(Hdf5TreeModel.VALUE_COLUMN, qt.QHeaderView.Interactive)
+ setResizeMode(Hdf5TreeModel.DESCRIPTION_COLUMN, qt.QHeaderView.Interactive)
+ setResizeMode(Hdf5TreeModel.NODE_COLUMN, qt.QHeaderView.Interactive)
+ setResizeMode(Hdf5TreeModel.LINK_COLUMN, qt.QHeaderView.Interactive)
def setAutoResizeColumns(self, autoResize):
"""Enable/disable auto-resize. When auto-resized, the header take care
diff --git a/silx/gui/hdf5/Hdf5Item.py b/silx/gui/hdf5/Hdf5Item.py
index 40793a4..f131f61 100644
--- a/silx/gui/hdf5/Hdf5Item.py
+++ b/silx/gui/hdf5/Hdf5Item.py
@@ -25,10 +25,9 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "20/01/2017"
+__date__ = "26/09/2017"
-import numpy
import logging
import collections
from .. import qt
@@ -37,6 +36,7 @@ from . import _utils
from .Hdf5Node import Hdf5Node
import silx.io.utils
from silx.gui.data.TextFormatter import TextFormatter
+from ..hdf5.Hdf5Formatter import Hdf5Formatter
_logger = logging.getLogger(__name__)
@@ -47,6 +47,8 @@ except ImportError as e:
raise e
_formatter = TextFormatter()
+_hdf5Formatter = Hdf5Formatter(textFormatter=_formatter)
+# FIXME: The formatter should be an attribute of the Hdf5Model
class Hdf5Item(Hdf5Node):
@@ -55,7 +57,7 @@ class Hdf5Item(Hdf5Node):
tree structure.
"""
- def __init__(self, text, obj, parent, key=None, h5pyClass=None, isBroken=False, populateAll=False):
+ def __init__(self, text, obj, parent, key=None, h5pyClass=None, linkClass=None, populateAll=False):
"""
:param str text: text displayed
:param object obj: Pointer to h5py data. See the `obj` attribute.
@@ -63,9 +65,10 @@ class Hdf5Item(Hdf5Node):
self.__obj = obj
self.__key = key
self.__h5pyClass = h5pyClass
- self.__isBroken = isBroken
+ self.__isBroken = obj is None and h5pyClass is None
self.__error = None
self.__text = text
+ self.__linkClass = linkClass
Hdf5Node.__init__(self, parent, populateAll=populateAll)
@property
@@ -88,16 +91,26 @@ class Hdf5Item(Hdf5Node):
:rtype: h5py.File or h5py.Dataset or h5py.Group
"""
- if self.__h5pyClass is None:
+ if self.__h5pyClass is None and self.obj is not None:
self.__h5pyClass = silx.io.utils.get_h5py_class(self.obj)
return self.__h5pyClass
+ @property
+ def linkClass(self):
+ """Returns the link class object of this node
+
+ :type: h5py.SoftLink or h5py.HardLink or h5py.ExternalLink or None
+ """
+ return self.__linkClass
+
def isGroupObj(self):
"""Returns true if the stored HDF5 object is a group (contains sub
groups or datasets).
:rtype: bool
"""
+ if self.h5pyClass is None:
+ return False
return issubclass(self.h5pyClass, h5py.Group)
def isBrokenObj(self):
@@ -111,6 +124,14 @@ class Hdf5Item(Hdf5Node):
"""
return self.__isBroken
+ def _getFormatter(self):
+ """
+ Returns an Hdf5Formatter
+
+ :rtype: Hdf5Formatter
+ """
+ return _hdf5Formatter
+
def _expectedChildCount(self):
if self.isGroupObj():
return len(self.obj)
@@ -158,6 +179,22 @@ class Hdf5Item(Hdf5Node):
self.__isBroken = True
else:
self.__obj = obj
+ if not self.isGroupObj():
+ try:
+ # pre-fetch of the data
+ if obj.shape is None:
+ pass
+ elif obj.shape == tuple():
+ obj[()]
+ else:
+ if obj.compression is None and obj.size > 0:
+ key = tuple([0] * len(obj.shape))
+ obj[key]
+ except Exception as e:
+ _logger.debug(e, exc_info=True)
+ message = "%s broken. %s" % (self.__obj.name, e.args[0])
+ self.__error = message
+ self.__isBroken = True
self.__key = None
@@ -166,15 +203,15 @@ class Hdf5Item(Hdf5Node):
for name in self.obj:
try:
class_ = self.obj.get(name, getclass=True)
- has_error = False
+ link = self.obj.get(name, getclass=True, getlink=True)
except Exception as e:
- _logger.error("Internal h5py error", exc_info=True)
+ _logger.warn("Internal h5py error", exc_info=True)
+ class_ = None
try:
- class_ = self.obj.get(name, getclass=True, getlink=True)
+ link = self.obj.get(name, getclass=True, getlink=True)
except Exception as e:
- class_ = h5py.HardLink
- has_error = True
- item = Hdf5Item(text=name, obj=None, parent=self, key=name, h5pyClass=class_, isBroken=has_error)
+ link = h5py.HardLink
+ item = Hdf5Item(text=name, obj=None, parent=self, key=name, h5pyClass=class_, linkClass=link)
self.appendChild(item)
def hasChildren(self):
@@ -191,6 +228,8 @@ class Hdf5Item(Hdf5Node):
:rtype: qt.QIcon
"""
+ # Pre-fetch the object, in case it is broken
+ obj = self.obj
style = qt.QApplication.style()
if self.__isBroken:
icon = style.standardIcon(qt.QStyle.SP_MessageBoxCritical)
@@ -205,99 +244,53 @@ class Hdf5Item(Hdf5Node):
elif issubclass(class_, h5py.ExternalLink):
return style.standardIcon(qt.QStyle.SP_FileLinkIcon)
elif issubclass(class_, h5py.Dataset):
- if len(self.obj.shape) < 4:
- name = "item-%ddim" % len(self.obj.shape)
+ if obj.shape is None:
+ name = "item-none"
+ elif len(obj.shape) < 4:
+ name = "item-%ddim" % len(obj.shape)
else:
name = "item-ndim"
- if str(self.obj.dtype) == "object":
- name = "item-object"
icon = icons.getQIcon(name)
return icon
return None
- def _humanReadableShape(self, dataset):
- if dataset.shape == tuple():
- return "scalar"
- shape = [str(i) for i in dataset.shape]
- text = u" \u00D7 ".join(shape)
- return text
-
- def _humanReadableValue(self, dataset):
- if dataset.shape == tuple():
- numpy_object = dataset[()]
- text = _formatter.toString(numpy_object)
- else:
- if dataset.size < 5 and dataset.compression is None:
- numpy_object = dataset[0:5]
- text = _formatter.toString(numpy_object)
- else:
- dimension = len(dataset.shape)
- if dataset.compression is not None:
- text = "Compressed %dD data" % dimension
- else:
- text = "%dD data" % dimension
- return text
-
- def _humanReadableDType(self, dtype, full=False):
- if dtype.type == numpy.string_:
- text = "string"
- elif dtype.type == numpy.unicode_:
- text = "string"
- elif dtype.type == numpy.object_:
- text = "object"
- elif dtype.type == numpy.bool_:
- text = "bool"
- elif dtype.type == numpy.void:
- if dtype.fields is None:
- text = "raw"
- else:
- if not full:
- text = "compound"
- else:
- compound = [d[0] for d in dtype.fields.values()]
- compound = [self._humanReadableDType(d) for d in compound]
- text = "compound(%s)" % ", ".join(compound)
- else:
- text = str(dtype)
- return text
-
- def _humanReadableType(self, dataset, full=False):
- return self._humanReadableDType(dataset.dtype, full)
-
- def _setTooltipAttributes(self, attributeDict):
+ def _createTooltipAttributes(self):
"""
Add key/value attributes that will be displayed in the item tooltip
:param Dict[str,str] attributeDict: Key/value attributes
"""
+ attributeDict = collections.OrderedDict()
+
if issubclass(self.h5pyClass, h5py.Dataset):
- attributeDict["Title"] = "HDF5 Dataset"
+ attributeDict["#Title"] = "HDF5 Dataset"
attributeDict["Name"] = self.basename
attributeDict["Path"] = self.obj.name
- attributeDict["Shape"] = self._humanReadableShape(self.obj)
- attributeDict["Value"] = self._humanReadableValue(self.obj)
- attributeDict["Data type"] = self._humanReadableType(self.obj, full=True)
+ attributeDict["Shape"] = self._getFormatter().humanReadableShape(self.obj)
+ attributeDict["Value"] = self._getFormatter().humanReadableValue(self.obj)
+ attributeDict["Data type"] = self._getFormatter().humanReadableType(self.obj, full=True)
elif issubclass(self.h5pyClass, h5py.Group):
- attributeDict["Title"] = "HDF5 Group"
+ attributeDict["#Title"] = "HDF5 Group"
attributeDict["Name"] = self.basename
attributeDict["Path"] = self.obj.name
elif issubclass(self.h5pyClass, h5py.File):
- attributeDict["Title"] = "HDF5 File"
+ attributeDict["#Title"] = "HDF5 File"
attributeDict["Name"] = self.basename
attributeDict["Path"] = "/"
elif isinstance(self.obj, h5py.ExternalLink):
- attributeDict["Title"] = "HDF5 External Link"
+ attributeDict["#Title"] = "HDF5 External Link"
attributeDict["Name"] = self.basename
attributeDict["Path"] = self.obj.name
attributeDict["Linked path"] = self.obj.path
attributeDict["Linked file"] = self.obj.filename
elif isinstance(self.obj, h5py.SoftLink):
- attributeDict["Title"] = "HDF5 Soft Link"
+ attributeDict["#Title"] = "HDF5 Soft Link"
attributeDict["Name"] = self.basename
attributeDict["Path"] = self.obj.name
attributeDict["Linked path"] = self.obj.path
else:
pass
+ return attributeDict
def _getDefaultTooltip(self):
"""Returns the default tooltip
@@ -308,10 +301,8 @@ class Hdf5Item(Hdf5Node):
self.obj # lazy loading of the object
return self.__error
- attrs = collections.OrderedDict()
- self._setTooltipAttributes(attrs)
-
- title = attrs.pop("Title", None)
+ attrs = self._createTooltipAttributes()
+ title = attrs.pop("#Title", None)
if len(attrs) > 0:
tooltip = _utils.htmlFromDict(attrs, title=title)
else:
@@ -342,7 +333,7 @@ class Hdf5Item(Hdf5Node):
return ""
class_ = self.h5pyClass
if issubclass(class_, h5py.Dataset):
- text = self._humanReadableType(self.obj)
+ text = self._getFormatter().humanReadableType(self.obj)
else:
text = ""
return text
@@ -361,7 +352,7 @@ class Hdf5Item(Hdf5Node):
class_ = self.h5pyClass
if not issubclass(class_, h5py.Dataset):
return ""
- return self._humanReadableShape(self.obj)
+ return self._getFormatter().humanReadableShape(self.obj)
return None
def dataValue(self, role):
@@ -375,7 +366,7 @@ class Hdf5Item(Hdf5Node):
return ""
if not issubclass(self.h5pyClass, h5py.Dataset):
return ""
- return self._humanReadableValue(self.obj)
+ return self._getFormatter().humanReadableValue(self.obj)
return None
def dataDescription(self, role):
@@ -412,10 +403,41 @@ class Hdf5Item(Hdf5Node):
if role == qt.Qt.TextAlignmentRole:
return qt.Qt.AlignTop | qt.Qt.AlignLeft
if role == qt.Qt.DisplayRole:
+ if self.isBrokenObj():
+ return ""
class_ = self.h5pyClass
text = class_.__name__.split(".")[-1]
return text
if role == qt.Qt.ToolTipRole:
class_ = self.h5pyClass
+ if class_ is None:
+ return ""
return "Class name: %s" % self.__class__
return None
+
+ def dataLink(self, role):
+ """Data for the link column
+
+ Overwrite it to implement the content of the 'link' column.
+
+ :rtype: qt.QVariant
+ """
+ if role == qt.Qt.DecorationRole:
+ return None
+ if role == qt.Qt.TextAlignmentRole:
+ return qt.Qt.AlignTop | qt.Qt.AlignLeft
+ if role == qt.Qt.DisplayRole:
+ link = self.linkClass
+ if link is None:
+ return ""
+ elif link is h5py.ExternalLink:
+ return "External"
+ elif link is h5py.SoftLink:
+ return "Soft"
+ elif link is h5py.HardLink:
+ return ""
+ else:
+ return link.__name__
+ if role == qt.Qt.ToolTipRole:
+ return None
+ return None
diff --git a/silx/gui/hdf5/Hdf5Node.py b/silx/gui/hdf5/Hdf5Node.py
index 31bb097..0fcb407 100644
--- a/silx/gui/hdf5/Hdf5Node.py
+++ b/silx/gui/hdf5/Hdf5Node.py
@@ -25,7 +25,9 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "23/09/2016"
+__date__ = "16/06/2017"
+
+import weakref
class Hdf5Node(object):
@@ -43,7 +45,9 @@ class Hdf5Node(object):
everything is lazy loaded.
"""
self.__child = None
- self.__parent = parent
+ self.__parent = None
+ if parent is not None:
+ self.__parent = weakref.ref(parent)
if populateAll:
self.__child = []
self._populateChild(populateAll=True)
@@ -54,7 +58,12 @@ class Hdf5Node(object):
:rtype: Hdf5Node
"""
- return self.__parent
+ if self.__parent is None:
+ return None
+ parent = self.__parent()
+ if parent is None:
+ self.__parent = parent
+ return parent
def setParent(self, parent):
"""Redefine the parent of the node.
@@ -63,7 +72,10 @@ class Hdf5Node(object):
:param Hdf5Node parent: The new parent
"""
- self.__parent = parent
+ if parent is None:
+ self.__parent = None
+ else:
+ self.__parent = weakref.ref(parent)
def appendChild(self, child):
"""Append a child to the node.
@@ -208,3 +220,12 @@ class Hdf5Node(object):
:rtype: qt.QVariant
"""
return None
+
+ def dataLink(self, role):
+ """Data for the link column
+
+ Overwrite it to implement the content of the 'link' column.
+
+ :rtype: qt.QVariant
+ """
+ return None
diff --git a/silx/gui/hdf5/Hdf5TreeModel.py b/silx/gui/hdf5/Hdf5TreeModel.py
index fb5de06..41fa91c 100644
--- a/silx/gui/hdf5/Hdf5TreeModel.py
+++ b/silx/gui/hdf5/Hdf5TreeModel.py
@@ -25,7 +25,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "19/12/2016"
+__date__ = "22/09/2017"
import os
@@ -71,6 +71,25 @@ else:
return x
+def _createRootLabel(h5obj):
+ """
+ Create label for the very first npde of the tree.
+
+ :param h5obj: The h5py object to display in the GUI
+ :type h5obj: h5py-like object
+ :rtpye: str
+ """
+ if silx_io.is_file(h5obj):
+ label = os.path.basename(h5obj.filename)
+ else:
+ filename = os.path.basename(h5obj.file.filename)
+ path = h5obj.name
+ if path.startswith("/"):
+ path = path[1:]
+ label = "%s::%s" % (filename, path)
+ return label
+
+
class LoadingItemRunnable(qt.QRunnable):
"""Runner to process item loading from a file"""
@@ -107,12 +126,7 @@ class LoadingItemRunnable(qt.QRunnable):
:param h5py.File h5obj: The h5py object to display in the GUI
:rtpye: Hdf5Node
"""
- if silx_io.is_file(h5obj):
- text = os.path.basename(h5obj.filename)
- else:
- filename = os.path.basename(h5obj.file.filename)
- path = h5obj.name
- text = "%s::%s" % (filename, path)
+ text = _createRootLabel(h5obj)
item = Hdf5Item(text=text, obj=h5obj, parent=oldItem.parent, populateAll=True)
return item
@@ -121,6 +135,7 @@ class LoadingItemRunnable(qt.QRunnable):
"""Process the file loading. The worker is used as holder
of the data and the signal. The result is sent as a signal.
"""
+ h5file = None
try:
h5file = silx_io.open(self.filename)
newItem = self.__loadItemTree(self.oldItem, h5file)
@@ -129,6 +144,8 @@ class LoadingItemRunnable(qt.QRunnable):
# Should be logged
error = e
newItem = None
+ if h5file is not None:
+ h5file.close()
# Take care of None value in case of PySide
newItem = _wrapNone(newItem)
@@ -174,6 +191,9 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
NODE_COLUMN = 5
"""Column id containing HDF5 node type"""
+ LINK_COLUMN = 6
+ """Column id containing HDF5 link type"""
+
COLUMN_IDS = [
NAME_COLUMN,
TYPE_COLUMN,
@@ -181,20 +201,21 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
VALUE_COLUMN,
DESCRIPTION_COLUMN,
NODE_COLUMN,
+ LINK_COLUMN,
]
"""List of logical columns available"""
def __init__(self, parent=None):
super(Hdf5TreeModel, self).__init__(parent)
- self.treeView = parent
- self.header_labels = [None] * 6
+ self.header_labels = [None] * len(self.COLUMN_IDS)
self.header_labels[self.NAME_COLUMN] = 'Name'
self.header_labels[self.TYPE_COLUMN] = 'Type'
self.header_labels[self.SHAPE_COLUMN] = 'Shape'
self.header_labels[self.VALUE_COLUMN] = 'Value'
self.header_labels[self.DESCRIPTION_COLUMN] = 'Description'
self.header_labels[self.NODE_COLUMN] = 'Node'
+ self.header_labels[self.LINK_COLUMN] = 'Link'
# Create items
self.__root = Hdf5Node()
@@ -205,14 +226,36 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
self.__animatedIcon.iconChanged.connect(self.__updateLoadingItems)
self.__runnerSet = set([])
- # store used icons to avoid to avoid the cache to release it
+ # store used icons to avoid the cache to release it
self.__icons = []
+ self.__icons.append(icons.getQIcon("item-none"))
self.__icons.append(icons.getQIcon("item-0dim"))
self.__icons.append(icons.getQIcon("item-1dim"))
self.__icons.append(icons.getQIcon("item-2dim"))
self.__icons.append(icons.getQIcon("item-3dim"))
self.__icons.append(icons.getQIcon("item-ndim"))
- self.__icons.append(icons.getQIcon("item-object"))
+
+ self.__openedFiles = []
+ """Store the list of files opened by the model itself."""
+ # FIXME: It should managed one by one by Hdf5Item itself
+
+ def __del__(self):
+ self._closeOpened()
+ s = super(Hdf5TreeModel, self)
+ if hasattr(s, "__del__"):
+ # else it fail on Python 3
+ s.__del__()
+
+ def _closeOpened(self):
+ """Close files which was opened by this model.
+
+ This function may be removed in the future.
+
+ File are opened by the model when it was inserted using
+ `insertFileAsync`, `insertFile`, `appendFile`."""
+ for h5file in self.__openedFiles:
+ h5file.close()
+ self.__openedFiles = []
def __updateLoadingItems(self, icon):
for i in range(self.__root.childCount()):
@@ -240,6 +283,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
self.__root.removeChildAtIndex(row)
self.endRemoveRows()
if newItem is not None:
+ self.__openedFiles.append(newItem.obj)
self.beginInsertRows(rootIndex, row, row)
self.__root.insertChild(row, newItem)
self.endInsertRows()
@@ -423,11 +467,13 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
return node.dataDescription(role)
elif index.column() == self.NODE_COLUMN:
return node.dataNode(role)
+ elif index.column() == self.LINK_COLUMN:
+ return node.dataLink(role)
else:
return None
def columnCount(self, parent=qt.QModelIndex()):
- return len(self.header_labels)
+ return len(self.COLUMN_IDS)
def hasChildren(self, parent=qt.QModelIndex()):
node = self.nodeFromIndex(parent)
@@ -536,12 +582,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
or any other class of h5py file structure.
"""
if text is None:
- if silx_io.is_file(h5pyObject):
- text = os.path.basename(h5pyObject.filename)
- else:
- filename = os.path.basename(h5pyObject.file.filename)
- path = h5pyObject.name
- text = "%s::%s" % (filename, path)
+ text = _createRootLabel(h5pyObject)
if row == -1:
row = self.__root.childCount()
self.insertNode(row, Hdf5Item(text=text, obj=h5pyObject, parent=self.__root))
@@ -572,6 +613,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
"""
try:
h5file = silx_io.open(filename)
+ self.__openedFiles.append(h5file)
self.insertH5pyObject(h5file, row=row)
except IOError:
_logger.debug("File '%s' can't be read.", filename, exc_info=True)
diff --git a/silx/gui/hdf5/Hdf5TreeView.py b/silx/gui/hdf5/Hdf5TreeView.py
index 09f6fcf..0a4198e 100644
--- a/silx/gui/hdf5/Hdf5TreeView.py
+++ b/silx/gui/hdf5/Hdf5TreeView.py
@@ -25,7 +25,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "27/09/2016"
+__date__ = "20/09/2017"
import logging
@@ -43,6 +43,8 @@ _logger = logging.getLogger(__name__)
class Hdf5TreeView(qt.QTreeView):
"""TreeView which allow to browse HDF5 file structure.
+ .. image:: img/Hdf5TreeView.png
+
It provides columns width auto-resizing and additional
signals.
@@ -192,6 +194,87 @@ class Hdf5TreeView(qt.QTreeView):
continue
yield _utils.H5Node(item)
+ def setSelectedH5Node(self, h5Object):
+ """
+ Select the specified node of the tree using an h5py node.
+
+ - If the item is found, parent items are expended, and then the item
+ is selected.
+ - If the item is not found, the selection do not change.
+ - A none argument allow to deselect everything
+
+ :param h5py.Npde h5Object: The node to select
+ """
+ if h5Object is None:
+ self.setCurrentIndex(qt.QModelIndex())
+ return
+
+ filename = h5Object.file.filename
+
+ # Seach for the right roots
+ rootIndices = []
+ model = self.model()
+ for index in range(model.rowCount(qt.QModelIndex())):
+ index = model.index(index, 0, qt.QModelIndex())
+ obj = model.data(index, Hdf5TreeModel.H5PY_OBJECT_ROLE)
+ if obj.file.filename == filename:
+ # We can have many roots with different subtree of the same
+ # root
+ rootIndices.append(index)
+
+ if len(rootIndices) == 0:
+ # No root found
+ return
+
+ path = h5Object.name + "/"
+ path = path.replace("//", "/")
+
+ # Search for the right node
+ found = False
+ foundIndices = []
+ for _ in range(1000 * len(rootIndices)):
+ # Avoid too much iterations, in case of recurssive links
+ if len(foundIndices) == 0:
+ if len(rootIndices) == 0:
+ # Nothing found
+ break
+ # Start fron a new root
+ foundIndices.append(rootIndices.pop(0))
+
+ obj = model.data(index, Hdf5TreeModel.H5PY_OBJECT_ROLE)
+ p = obj.name + "/"
+ p = p.replace("//", "/")
+ if path == p:
+ found = True
+ break
+
+ parentIndex = foundIndices[-1]
+ for index in range(model.rowCount(parentIndex)):
+ index = model.index(index, 0, parentIndex)
+ obj = model.data(index, Hdf5TreeModel.H5PY_OBJECT_ROLE)
+
+ p = obj.name + "/"
+ p = p.replace("//", "/")
+ if path == p:
+ foundIndices.append(index)
+ found = True
+ break
+ elif path.startswith(p):
+ foundIndices.append(index)
+ break
+ else:
+ # Nothing found, start again with another root
+ foundIndices = []
+
+ if found:
+ break
+
+ if found:
+ # Update the GUI
+ for index in foundIndices[:-1]:
+ self.expand(index)
+ self.setCurrentIndex(foundIndices[-1])
+
def mousePressEvent(self, event):
"""Override mousePressEvent to provide a consistante compatible API
between Qt4 and Qt5
diff --git a/silx/gui/hdf5/NexusSortFilterProxyModel.py b/silx/gui/hdf5/NexusSortFilterProxyModel.py
index 9a4268c..49a22d3 100644
--- a/silx/gui/hdf5/NexusSortFilterProxyModel.py
+++ b/silx/gui/hdf5/NexusSortFilterProxyModel.py
@@ -25,7 +25,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "12/04/2017"
+__date__ = "16/06/2017"
import logging
@@ -86,7 +86,8 @@ class NexusSortFilterProxyModel(qt.QSortFilterProxyModel):
def __isNXentry(self, node):
"""Returns true if the node is an NXentry"""
- if not issubclass(node.h5pyClass, h5py.Group):
+ class_ = node.h5pyClass
+ if class_ is None or not issubclass(node.h5pyClass, h5py.Group):
return False
nxClass = node.obj.attrs.get("NX_class", None)
return nxClass == "NXentry"
diff --git a/silx/gui/hdf5/_utils.py b/silx/gui/hdf5/_utils.py
index af9c79f..048aa20 100644
--- a/silx/gui/hdf5/_utils.py
+++ b/silx/gui/hdf5/_utils.py
@@ -28,11 +28,10 @@ package `silx.gui.hdf5` package.
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "26/04/2017"
+__date__ = "29/09/2017"
import logging
-import numpy
from .. import qt
import silx.io.utils
from silx.utils.html import escape
@@ -138,10 +137,61 @@ class H5Node(object):
:param Hdf5Item h5py_item: An Hdf5Item
"""
self.__h5py_object = h5py_item.obj
+ self.__h5py_target = None
self.__h5py_item = h5py_item
def __getattr__(self, name):
- return object.__getattribute__(self.__h5py_object, name)
+ if hasattr(self.__h5py_object, name):
+ attr = getattr(self.__h5py_object, name)
+ return attr
+ raise AttributeError("H5Node has no attribute %s" % name)
+
+ def __get_target(self, obj):
+ """
+ Return the actual physical target of the provided object.
+
+ Objects can contains links in the middle of the path, this function
+ check each groups and remove this prefix in case of the link by the
+ link of the path.
+
+ :param obj: A valid h5py object (File, group or dataset)
+ :type obj: h5py.Dataset or h5py.Group or h5py.File
+ :rtype: h5py.Dataset or h5py.Group or h5py.File
+ """
+ elements = obj.name.split("/")
+ if obj.name == "/":
+ return obj
+ elif obj.name.startswith("/"):
+ elements.pop(0)
+ path = ""
+ while len(elements) > 0:
+ e = elements.pop(0)
+ path = path + "/" + e
+ link = obj.parent.get(path, getlink=True)
+
+ if isinstance(link, h5py.ExternalLink):
+ subpath = "/".join(elements)
+ external_obj = obj.parent.get(self.basename + "/" + subpath)
+ return self.__get_target(external_obj)
+ elif silx.io.utils.is_softlink(link):
+ # Restart from this stat
+ path = ""
+ root_elements = link.path.split("/")
+ if link.path == "/":
+ root_elements = []
+ elif link.path.startswith("/"):
+ root_elements.pop(0)
+ for name in reversed(root_elements):
+ elements.insert(0, name)
+
+ return obj.file[path]
+
+ @property
+ def h5py_target(self):
+ if self.__h5py_target is not None:
+ return self.__h5py_target
+ self.__h5py_target = self.__get_target(self.__h5py_object)
+ return self.__h5py_target
@property
def h5py_object(self):
@@ -170,8 +220,18 @@ class H5Node(object):
return self.__h5py_object.name.split("/")[-1]
@property
+ def is_broken(self):
+ """Returns true if the node is a broken link.
+
+ :rtype: bool
+ """
+ if self.__h5py_item is None:
+ raise RuntimeError("h5py_item is not defined")
+ return self.__h5py_item.isBrokenObj()
+
+ @property
def local_name(self):
- """Returns the local path of this h5py node.
+ """Returns the path from the master file root to this node.
For links, this path is not equal to the h5py one.
@@ -183,34 +243,46 @@ class H5Node(object):
result = []
item = self.__h5py_item
while item is not None:
- if issubclass(item.h5pyClass, h5py.File):
+ # stop before the root item (item without parent)
+ if item.parent.parent is None:
+ name = item.obj.name
+ if name != "/":
+ result.append(item.obj.name)
break
- result.append(item.basename)
+ else:
+ result.append(item.basename)
item = item.parent
if item is None:
raise RuntimeError("The item does not have parent holding h5py.File")
if result == []:
return "/"
- result.append("")
+ if not result[-1].startswith("/"):
+ result.append("")
result.reverse()
- return "/".join(result)
+ name = "/".join(result)
+ return name
- def __file_item(self):
- """Returns the parent item holding the :class:`h5py.File` object
+ def __get_local_file(self):
+ """Returns the file of the root of this tree
:rtype: h5py.File
- :raises RuntimeException: If no file are found
"""
item = self.__h5py_item
- while item is not None:
- if issubclass(item.h5pyClass, h5py.File):
- return item
+ while item.parent.parent is not None:
+ class_ = item.h5pyClass
+ if class_ is not None and issubclass(class_, h5py.File):
+ break
item = item.parent
- raise RuntimeError("The item does not have parent holding h5py.File")
+
+ class_ = item.h5pyClass
+ if class_ is not None and issubclass(class_, h5py.File):
+ return item.obj
+ else:
+ return item.obj.file
@property
def local_file(self):
- """Returns the local :class:`h5py.File` object.
+ """Returns the master file in which is this node.
For path containing external links, this file is not equal to the h5py
one.
@@ -218,12 +290,11 @@ class H5Node(object):
:rtype: h5py.File
:raises RuntimeException: If no file are found
"""
- item = self.__file_item()
- return item.obj
+ return self.__get_local_file()
@property
def local_filename(self):
- """Returns the local filename of the h5py node.
+ """Returns the filename from the master file of this node.
For path containing external links, this path is not equal to the
filename provided by h5py.
@@ -235,13 +306,84 @@ class H5Node(object):
@property
def local_basename(self):
- """Returns the local filename of the h5py node.
+ """Returns the basename from the master file root to this node.
For path containing links, this basename can be different than the
basename provided by h5py.
:rtype: str
"""
- if issubclass(self.__h5py_item.h5pyClass, h5py.File):
+ class_ = self.__h5py_item.h5pyClass
+ if class_ is not None and issubclass(class_, h5py.File):
return ""
return self.__h5py_item.basename
+
+ @property
+ def physical_file(self):
+ """Returns the physical file in which is this node.
+
+ .. versionadded:: 0.6
+
+ :rtype: h5py.File
+ :raises RuntimeError: If no file are found
+ """
+ if isinstance(self.__h5py_object, h5py.ExternalLink):
+ # It means the link is broken
+ raise RuntimeError("No file node found")
+ if isinstance(self.__h5py_object, h5py.SoftLink):
+ # It means the link is broken
+ return self.local_file
+
+ physical_obj = self.h5py_target
+ return physical_obj.file
+
+ @property
+ def physical_name(self):
+ """Returns the path from the location this h5py node is physically
+ stored.
+
+ For broken links, this filename can be different from the
+ filename provided by h5py.
+
+ :rtype: str
+ """
+ if isinstance(self.__h5py_object, h5py.ExternalLink):
+ # It means the link is broken
+ return self.__h5py_object.path
+ if isinstance(self.__h5py_object, h5py.SoftLink):
+ # It means the link is broken
+ return self.__h5py_object.path
+
+ physical_obj = self.h5py_target
+ return physical_obj.name
+
+ @property
+ def physical_filename(self):
+ """Returns the filename from the location this h5py node is physically
+ stored.
+
+ For broken links, this filename can be different from the
+ filename provided by h5py.
+
+ :rtype: str
+ """
+ if isinstance(self.__h5py_object, h5py.ExternalLink):
+ # It means the link is broken
+ return self.__h5py_object.filename
+ if isinstance(self.__h5py_object, h5py.SoftLink):
+ # It means the link is broken
+ return self.local_file.filename
+
+ return self.physical_file.filename
+
+ @property
+ def physical_basename(self):
+ """Returns the basename from the location this h5py node is physically
+ stored.
+
+ For broken links, this basename can be different from the
+ basename provided by h5py.
+
+ :rtype: str
+ """
+ return self.physical_name.split("/")[-1]
diff --git a/silx/gui/hdf5/test/_mock.py b/silx/gui/hdf5/test/_mock.py
deleted file mode 100644
index eada590..0000000
--- a/silx/gui/hdf5/test/_mock.py
+++ /dev/null
@@ -1,130 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-"""Mock for silx.gui.hdf5 module"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "12/04/2017"
-
-
-import numpy
-try:
- import h5py
-except ImportError:
- h5py = None
-
-
-class Node(object):
-
- def __init__(self, basename, parent, h5py_class):
- self.basename = basename
- self.h5py_class = h5py_class
- self.attrs = {}
- self.parent = parent
- if parent is not None:
- self.parent._add(self)
-
- @property
- def name(self):
- if self.parent is None:
- return self.basename
- if self.parent.name == "":
- return self.basename
- return self.parent.name + "/" + self.basename
-
- @property
- def file(self):
- if self.parent is None:
- return self
- return self.parent.file
-
-
-class Group(Node):
- """Mock an h5py Group"""
-
- def __init__(self, name, parent, h5py_class=h5py.Group):
- super(Group, self).__init__(name, parent, h5py_class)
- self.__items = {}
-
- def _add(self, node):
- self.__items[node.basename] = node
-
- def __getitem__(self, key):
- return self.__items[key]
-
- def __iter__(self):
- for k in self.__items:
- yield k
-
- def __len__(self):
- return len(self.__items)
-
- def get(self, name, getclass=False, getlink=False):
- result = self.__items[name]
- if getclass:
- return result.h5py_class
- return result
-
- def create_dataset(self, name, data):
- return Dataset(name, self, data)
-
- def create_group(self, name):
- return Group(name, self)
-
- def create_NXentry(self, name):
- group = Group(name, self)
- group.attrs["NX_class"] = "NXentry"
- return group
-
-
-class File(Group):
- """Mock an h5py File"""
-
- def __init__(self, filename):
- super(File, self).__init__("", None, h5py.File)
- self.filename = filename
-
-
-class Dataset(Node):
- """Mock an h5py Dataset"""
-
- def __init__(self, name, parent, value):
- super(Dataset, self).__init__(name, parent, h5py.Dataset)
- self.__value = value
- self.shape = self.__value.shape
- self.dtype = self.__value.dtype
- self.size = self.__value.size
- self.compression = None
- self.compression_opts = None
-
- def __getitem__(self, key):
- if not isinstance(self.__value, numpy.ndarray):
- if key == tuple():
- return self.__value
- elif key == Ellipsis:
- return numpy.array(self.__value)
- else:
- raise ValueError("Bad key")
- return self.__value[key]
diff --git a/silx/gui/hdf5/test/test_hdf5.py b/silx/gui/hdf5/test/test_hdf5.py
index 3bf4897..8e375f2 100644
--- a/silx/gui/hdf5/test/test_hdf5.py
+++ b/silx/gui/hdf5/test/test_hdf5.py
@@ -26,7 +26,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "12/04/2017"
+__date__ = "22/09/2017"
import time
@@ -34,11 +34,12 @@ import os
import unittest
import tempfile
import numpy
+import shutil
from contextlib import contextmanager
from silx.gui import qt
from silx.gui.test.utils import TestCaseQt
from silx.gui import hdf5
-from . import _mock
+from silx.io import commonh5
try:
import h5py
@@ -54,6 +55,13 @@ class _Holder(object):
_called += 1
+def create_NXentry(group, name):
+ attrs = {"NX_class": "NXentry"}
+ node = commonh5.Group(name, parent=group, attrs=attrs)
+ group.add_node(node)
+ return node
+
+
class TestHdf5TreeModel(TestCaseQt):
def setUp(self):
@@ -124,14 +132,14 @@ class TestHdf5TreeModel(TestCaseQt):
h5File.close()
def testInsertObject(self):
- h5 = _mock.File("/foo/bar/1.mock")
+ h5 = commonh5.File("/foo/bar/1.mock", "w")
model = hdf5.Hdf5TreeModel()
self.assertEquals(model.rowCount(qt.QModelIndex()), 0)
model.insertH5pyObject(h5)
self.assertEquals(model.rowCount(qt.QModelIndex()), 1)
def testRemoveObject(self):
- h5 = _mock.File("/foo/bar/1.mock")
+ h5 = commonh5.File("/foo/bar/1.mock", "w")
model = hdf5.Hdf5TreeModel()
self.assertEquals(model.rowCount(qt.QModelIndex()), 0)
model.insertH5pyObject(h5)
@@ -223,7 +231,7 @@ class TestHdf5TreeModel(TestCaseQt):
return model.data(index, qt.Qt.DisplayRole)
def testFileData(self):
- h5 = _mock.File("/foo/bar/1.mock")
+ h5 = commonh5.File("/foo/bar/1.mock", "w")
model = hdf5.Hdf5TreeModel()
model.insertH5pyObject(h5)
displayed = self.getRowDataAsDict(model, row=0)
@@ -236,7 +244,7 @@ class TestHdf5TreeModel(TestCaseQt):
self.assertEquals(displayed[hdf5.Hdf5TreeModel.NODE_COLUMN, qt.Qt.DisplayRole], "File")
def testGroupData(self):
- h5 = _mock.File("/foo/bar/1.mock")
+ h5 = commonh5.File("/foo/bar/1.mock", "w")
d = h5.create_group("foo")
d.attrs["desc"] = "fooo"
@@ -252,9 +260,9 @@ class TestHdf5TreeModel(TestCaseQt):
self.assertEquals(displayed[hdf5.Hdf5TreeModel.NODE_COLUMN, qt.Qt.DisplayRole], "Group")
def testDatasetData(self):
- h5 = _mock.File("/foo/bar/1.mock")
+ h5 = commonh5.File("/foo/bar/1.mock", "w")
value = numpy.array([1, 2, 3])
- d = h5.create_dataset("foo", value)
+ d = h5.create_dataset("foo", data=value)
model = hdf5.Hdf5TreeModel()
model.insertH5pyObject(d)
@@ -269,8 +277,8 @@ class TestHdf5TreeModel(TestCaseQt):
def testDropLastAsFirst(self):
model = hdf5.Hdf5TreeModel()
- h5_1 = _mock.File("/foo/bar/1.mock")
- h5_2 = _mock.File("/foo/bar/2.mock")
+ h5_1 = commonh5.File("/foo/bar/1.mock", "w")
+ h5_2 = commonh5.File("/foo/bar/2.mock", "w")
model.insertH5pyObject(h5_1)
model.insertH5pyObject(h5_2)
self.assertEquals(self.getItemName(model, 0), "1.mock")
@@ -283,8 +291,8 @@ class TestHdf5TreeModel(TestCaseQt):
def testDropFirstAsLast(self):
model = hdf5.Hdf5TreeModel()
- h5_1 = _mock.File("/foo/bar/1.mock")
- h5_2 = _mock.File("/foo/bar/2.mock")
+ h5_1 = commonh5.File("/foo/bar/1.mock", "w")
+ h5_2 = commonh5.File("/foo/bar/2.mock", "w")
model.insertH5pyObject(h5_1)
model.insertH5pyObject(h5_2)
self.assertEquals(self.getItemName(model, 0), "1.mock")
@@ -297,7 +305,7 @@ class TestHdf5TreeModel(TestCaseQt):
def testRootParent(self):
model = hdf5.Hdf5TreeModel()
- h5_1 = _mock.File("/foo/bar/1.mock")
+ h5_1 = commonh5.File("/foo/bar/1.mock", "w")
model.insertH5pyObject(h5_1)
index = model.index(0, 0, qt.QModelIndex())
index = model.parent(index)
@@ -318,10 +326,10 @@ class TestNexusSortFilterProxyModel(TestCaseQt):
def testNXentryStartTime(self):
"""Test NXentry with start_time"""
model = hdf5.Hdf5TreeModel()
- h5 = _mock.File("/foo/bar/1.mock")
- h5.create_NXentry("a").create_dataset("start_time", numpy.string_("2015"))
- h5.create_NXentry("b").create_dataset("start_time", numpy.string_("2013"))
- h5.create_NXentry("c").create_dataset("start_time", numpy.string_("2014"))
+ h5 = commonh5.File("/foo/bar/1.mock", "w")
+ create_NXentry(h5, "a").create_dataset("start_time", data=numpy.string_("2015"))
+ create_NXentry(h5, "b").create_dataset("start_time", data=numpy.string_("2013"))
+ create_NXentry(h5, "c").create_dataset("start_time", data=numpy.string_("2014"))
model.insertH5pyObject(h5)
proxy = hdf5.NexusSortFilterProxyModel()
@@ -333,10 +341,10 @@ class TestNexusSortFilterProxyModel(TestCaseQt):
def testNXentryStartTimeInArray(self):
"""Test NXentry with start_time"""
model = hdf5.Hdf5TreeModel()
- h5 = _mock.File("/foo/bar/1.mock")
- h5.create_NXentry("a").create_dataset("start_time", numpy.array([numpy.string_("2015")]))
- h5.create_NXentry("b").create_dataset("start_time", numpy.array([numpy.string_("2013")]))
- h5.create_NXentry("c").create_dataset("start_time", numpy.array([numpy.string_("2014")]))
+ h5 = commonh5.File("/foo/bar/1.mock", "w")
+ create_NXentry(h5, "a").create_dataset("start_time", data=numpy.array([numpy.string_("2015")]))
+ create_NXentry(h5, "b").create_dataset("start_time", data=numpy.array([numpy.string_("2013")]))
+ create_NXentry(h5, "c").create_dataset("start_time", data=numpy.array([numpy.string_("2014")]))
model.insertH5pyObject(h5)
proxy = hdf5.NexusSortFilterProxyModel()
@@ -348,10 +356,10 @@ class TestNexusSortFilterProxyModel(TestCaseQt):
def testNXentryEndTimeInArray(self):
"""Test NXentry with end_time"""
model = hdf5.Hdf5TreeModel()
- h5 = _mock.File("/foo/bar/1.mock")
- h5.create_NXentry("a").create_dataset("end_time", numpy.array([numpy.string_("2015")]))
- h5.create_NXentry("b").create_dataset("end_time", numpy.array([numpy.string_("2013")]))
- h5.create_NXentry("c").create_dataset("end_time", numpy.array([numpy.string_("2014")]))
+ h5 = commonh5.File("/foo/bar/1.mock", "w")
+ create_NXentry(h5, "a").create_dataset("end_time", data=numpy.array([numpy.string_("2015")]))
+ create_NXentry(h5, "b").create_dataset("end_time", data=numpy.array([numpy.string_("2013")]))
+ create_NXentry(h5, "c").create_dataset("end_time", data=numpy.array([numpy.string_("2014")]))
model.insertH5pyObject(h5)
proxy = hdf5.NexusSortFilterProxyModel()
@@ -363,10 +371,10 @@ class TestNexusSortFilterProxyModel(TestCaseQt):
def testNXentryName(self):
"""Test NXentry without start_time or end_time"""
model = hdf5.Hdf5TreeModel()
- h5 = _mock.File("/foo/bar/1.mock")
- h5.create_NXentry("a")
- h5.create_NXentry("c")
- h5.create_NXentry("b")
+ h5 = commonh5.File("/foo/bar/1.mock", "w")
+ create_NXentry(h5, "a")
+ create_NXentry(h5, "c")
+ create_NXentry(h5, "b")
model.insertH5pyObject(h5)
proxy = hdf5.NexusSortFilterProxyModel()
@@ -378,10 +386,10 @@ class TestNexusSortFilterProxyModel(TestCaseQt):
def testStartTime(self):
"""If it is not NXentry, start_time is not used"""
model = hdf5.Hdf5TreeModel()
- h5 = _mock.File("/foo/bar/1.mock")
- h5.create_group("a").create_dataset("start_time", numpy.string_("2015"))
- h5.create_group("b").create_dataset("start_time", numpy.string_("2013"))
- h5.create_group("c").create_dataset("start_time", numpy.string_("2014"))
+ h5 = commonh5.File("/foo/bar/1.mock", "w")
+ h5.create_group("a").create_dataset("start_time", data=numpy.string_("2015"))
+ h5.create_group("b").create_dataset("start_time", data=numpy.string_("2013"))
+ h5.create_group("c").create_dataset("start_time", data=numpy.string_("2014"))
model.insertH5pyObject(h5)
proxy = hdf5.NexusSortFilterProxyModel()
@@ -392,7 +400,7 @@ class TestNexusSortFilterProxyModel(TestCaseQt):
def testName(self):
model = hdf5.Hdf5TreeModel()
- h5 = _mock.File("/foo/bar/1.mock")
+ h5 = commonh5.File("/foo/bar/1.mock", "w")
h5.create_group("a")
h5.create_group("c")
h5.create_group("b")
@@ -406,7 +414,7 @@ class TestNexusSortFilterProxyModel(TestCaseQt):
def testNumber(self):
model = hdf5.Hdf5TreeModel()
- h5 = _mock.File("/foo/bar/1.mock")
+ h5 = commonh5.File("/foo/bar/1.mock", "w")
h5.create_group("a1")
h5.create_group("a20")
h5.create_group("a3")
@@ -420,7 +428,7 @@ class TestNexusSortFilterProxyModel(TestCaseQt):
def testMultiNumber(self):
model = hdf5.Hdf5TreeModel()
- h5 = _mock.File("/foo/bar/1.mock")
+ h5 = commonh5.File("/foo/bar/1.mock", "w")
h5.create_group("a1-1")
h5.create_group("a20-1")
h5.create_group("a3-1")
@@ -436,7 +444,7 @@ class TestNexusSortFilterProxyModel(TestCaseQt):
def testUnconsistantTypes(self):
model = hdf5.Hdf5TreeModel()
- h5 = _mock.File("/foo/bar/1.mock")
+ h5 = commonh5.File("/foo/bar/1.mock", "w")
h5.create_group("aaa100")
h5.create_group("100aaa")
model.insertH5pyObject(h5)
@@ -448,11 +456,235 @@ class TestNexusSortFilterProxyModel(TestCaseQt):
self.assertListEqual(names, ["100aaa", "aaa100"])
-class TestHdf5(TestCaseQt):
+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)
+ cls.h5File = h5py.File(cls.h5Filename, mode="r")
+ cls.model = cls.createModel(cls.h5File)
+
+ @classmethod
+ def createResource(cls, directory):
+ filename = os.path.join(directory, "base.h5")
+ externalFilename = os.path.join(directory, "base__external.h5")
+
+ externalh5 = h5py.File(externalFilename, mode="w")
+ externalh5["target/dataset"] = 50
+ externalh5["target/link"] = h5py.SoftLink("/target/dataset")
+ externalh5.close()
+
+ h5 = h5py.File(filename, mode="w")
+ h5["group/dataset"] = 50
+ h5["link/soft_link"] = h5py.SoftLink("/group/dataset")
+ h5["link/soft_link_to_group"] = h5py.SoftLink("/group")
+ h5["link/soft_link_to_link"] = h5py.SoftLink("/link/soft_link")
+ h5["link/soft_link_to_file"] = h5py.SoftLink("/")
+ h5["link/external_link"] = h5py.ExternalLink(externalFilename, "/target/dataset")
+ h5["link/external_link_to_link"] = h5py.ExternalLink(externalFilename, "/target/link")
+ h5["broken_link/external_broken_file"] = h5py.ExternalLink(externalFilename + "_not_exists", "/target/link")
+ h5["broken_link/external_broken_link"] = h5py.ExternalLink(externalFilename, "/target/not_exists")
+ h5["broken_link/soft_broken_link"] = h5py.SoftLink("/group/not_exists")
+ h5["broken_link/soft_link_to_broken_link"] = h5py.SoftLink("/group/not_exists")
+ h5.close()
+
+ return filename
+
+ @classmethod
+ def createModel(cls, h5pyFile):
+ model = hdf5.Hdf5TreeModel()
+ model.insertH5pyObject(h5pyFile)
+ return model
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.model = None
+ cls.h5File.close()
+ shutil.rmtree(cls.tmpDirectory)
+ super(TestH5Node, cls).tearDownClass()
+
+ def getIndexFromPath(self, model, path):
+ """
+ :param qt.QAbstractItemModel: model
+ """
+ index = qt.QModelIndex()
+ for name in path:
+ for row in range(model.rowCount(index)):
+ i = model.index(row, 0, index)
+ label = model.data(i)
+ if label == name:
+ index = i
+ break
+ else:
+ raise RuntimeError("Path not found")
+ return index
+
+ def getH5NodeFromPath(self, model, path):
+ index = self.getIndexFromPath(model, path)
+ item = model.data(index, hdf5.Hdf5TreeModel.H5PY_ITEM_ROLE)
+ h5node = hdf5.H5Node(item)
+ return h5node
+
+ def testFile(self):
+ path = ["base.h5"]
+ h5node = self.getH5NodeFromPath(self.model, path)
+
+ self.assertEqual(h5node.physical_filename, h5node.local_filename)
+ self.assertIn("base.h5", h5node.physical_filename)
+ self.assertEqual(h5node.physical_basename, "")
+ self.assertEqual(h5node.physical_name, "/")
+ self.assertEqual(h5node.local_basename, "")
+ self.assertEqual(h5node.local_name, "/")
+
+ def testGroup(self):
+ path = ["base.h5", "group"]
+ h5node = self.getH5NodeFromPath(self.model, path)
+
+ self.assertEqual(h5node.physical_filename, h5node.local_filename)
+ self.assertIn("base.h5", h5node.physical_filename)
+ self.assertEqual(h5node.physical_basename, "group")
+ self.assertEqual(h5node.physical_name, "/group")
+ self.assertEqual(h5node.local_basename, "group")
+ self.assertEqual(h5node.local_name, "/group")
+
+ def testDataset(self):
+ path = ["base.h5", "group", "dataset"]
+ h5node = self.getH5NodeFromPath(self.model, path)
+
+ self.assertEqual(h5node.physical_filename, h5node.local_filename)
+ self.assertIn("base.h5", h5node.physical_filename)
+ self.assertEqual(h5node.physical_basename, "dataset")
+ self.assertEqual(h5node.physical_name, "/group/dataset")
+ self.assertEqual(h5node.local_basename, "dataset")
+ self.assertEqual(h5node.local_name, "/group/dataset")
+
+ def testSoftLink(self):
+ path = ["base.h5", "link", "soft_link"]
+ h5node = self.getH5NodeFromPath(self.model, path)
+
+ self.assertEqual(h5node.physical_filename, h5node.local_filename)
+ self.assertIn("base.h5", h5node.physical_filename)
+ self.assertEqual(h5node.physical_basename, "dataset")
+ self.assertEqual(h5node.physical_name, "/group/dataset")
+ self.assertEqual(h5node.local_basename, "soft_link")
+ self.assertEqual(h5node.local_name, "/link/soft_link")
+
+ def testSoftLinkToLink(self):
+ path = ["base.h5", "link", "soft_link_to_link"]
+ h5node = self.getH5NodeFromPath(self.model, path)
+
+ self.assertEqual(h5node.physical_filename, h5node.local_filename)
+ self.assertIn("base.h5", h5node.physical_filename)
+ self.assertEqual(h5node.physical_basename, "dataset")
+ self.assertEqual(h5node.physical_name, "/group/dataset")
+ self.assertEqual(h5node.local_basename, "soft_link_to_link")
+ self.assertEqual(h5node.local_name, "/link/soft_link_to_link")
+
+ def testExternalLink(self):
+ path = ["base.h5", "link", "external_link"]
+ h5node = self.getH5NodeFromPath(self.model, path)
+
+ self.assertNotEqual(h5node.physical_filename, h5node.local_filename)
+ self.assertIn("base.h5", h5node.local_filename)
+ self.assertIn("base__external.h5", h5node.physical_filename)
+ self.assertEqual(h5node.physical_basename, "dataset")
+ self.assertEqual(h5node.physical_name, "/target/dataset")
+ self.assertEqual(h5node.local_basename, "external_link")
+ self.assertEqual(h5node.local_name, "/link/external_link")
+
+ def testExternalLinkToLink(self):
+ path = ["base.h5", "link", "external_link_to_link"]
+ h5node = self.getH5NodeFromPath(self.model, path)
+
+ self.assertNotEqual(h5node.physical_filename, h5node.local_filename)
+ self.assertIn("base.h5", h5node.local_filename)
+ self.assertIn("base__external.h5", h5node.physical_filename)
+
+ self.assertNotEqual(h5node.physical_filename, h5node.local_filename)
+ self.assertEqual(h5node.physical_basename, "dataset")
+ self.assertEqual(h5node.physical_name, "/target/dataset")
+ self.assertEqual(h5node.local_basename, "external_link_to_link")
+ self.assertEqual(h5node.local_name, "/link/external_link_to_link")
+
+ def testExternalBrokenFile(self):
+ path = ["base.h5", "broken_link", "external_broken_file"]
+ h5node = self.getH5NodeFromPath(self.model, path)
+
+ self.assertNotEqual(h5node.physical_filename, h5node.local_filename)
+ self.assertIn("base.h5", h5node.local_filename)
+ self.assertIn("not_exists", h5node.physical_filename)
+ self.assertEqual(h5node.physical_basename, "link")
+ self.assertEqual(h5node.physical_name, "/target/link")
+ self.assertEqual(h5node.local_basename, "external_broken_file")
+ self.assertEqual(h5node.local_name, "/broken_link/external_broken_file")
+
+ def testExternalBrokenLink(self):
+ path = ["base.h5", "broken_link", "external_broken_link"]
+ h5node = self.getH5NodeFromPath(self.model, path)
+
+ self.assertNotEqual(h5node.physical_filename, h5node.local_filename)
+ self.assertIn("base.h5", h5node.local_filename)
+ self.assertIn("__external", h5node.physical_filename)
+ self.assertEqual(h5node.physical_basename, "not_exists")
+ self.assertEqual(h5node.physical_name, "/target/not_exists")
+ self.assertEqual(h5node.local_basename, "external_broken_link")
+ self.assertEqual(h5node.local_name, "/broken_link/external_broken_link")
+
+ def testSoftBrokenLink(self):
+ path = ["base.h5", "broken_link", "soft_broken_link"]
+ h5node = self.getH5NodeFromPath(self.model, path)
+
+ self.assertEqual(h5node.physical_filename, h5node.local_filename)
+ self.assertIn("base.h5", h5node.physical_filename)
+ self.assertEqual(h5node.physical_basename, "not_exists")
+ self.assertEqual(h5node.physical_name, "/group/not_exists")
+ self.assertEqual(h5node.local_basename, "soft_broken_link")
+ self.assertEqual(h5node.local_name, "/broken_link/soft_broken_link")
+
+ def testSoftLinkToBrokenLink(self):
+ path = ["base.h5", "broken_link", "soft_link_to_broken_link"]
+ h5node = self.getH5NodeFromPath(self.model, path)
+
+ self.assertEqual(h5node.physical_filename, h5node.local_filename)
+ self.assertIn("base.h5", h5node.physical_filename)
+ self.assertEqual(h5node.physical_basename, "not_exists")
+ self.assertEqual(h5node.physical_name, "/group/not_exists")
+ self.assertEqual(h5node.local_basename, "soft_link_to_broken_link")
+ self.assertEqual(h5node.local_name, "/broken_link/soft_link_to_broken_link")
+
+ def testDatasetFromSoftLinkToGroup(self):
+ path = ["base.h5", "link", "soft_link_to_group", "dataset"]
+ h5node = self.getH5NodeFromPath(self.model, path)
+
+ self.assertEqual(h5node.physical_filename, h5node.local_filename)
+ self.assertIn("base.h5", h5node.physical_filename)
+ self.assertEqual(h5node.physical_basename, "dataset")
+ self.assertEqual(h5node.physical_name, "/group/dataset")
+ self.assertEqual(h5node.local_basename, "dataset")
+ self.assertEqual(h5node.local_name, "/link/soft_link_to_group/dataset")
+
+ def testDatasetFromSoftLinkToFile(self):
+ path = ["base.h5", "link", "soft_link_to_file", "link", "soft_link_to_group", "dataset"]
+ h5node = self.getH5NodeFromPath(self.model, path)
+
+ self.assertEqual(h5node.physical_filename, h5node.local_filename)
+ self.assertIn("base.h5", h5node.physical_filename)
+ self.assertEqual(h5node.physical_basename, "dataset")
+ self.assertEqual(h5node.physical_name, "/group/dataset")
+ self.assertEqual(h5node.local_basename, "dataset")
+ self.assertEqual(h5node.local_name, "/link/soft_link_to_file/link/soft_link_to_group/dataset")
+
+
+class TestHdf5TreeView(TestCaseQt):
"""Test to check that icons module."""
def setUp(self):
- super(TestHdf5, self).setUp()
+ super(TestHdf5TreeView, self).setUp()
if h5py is None:
self.skipTest("h5py is not available")
@@ -464,15 +696,147 @@ class TestHdf5(TestCaseQt):
view = hdf5.Hdf5TreeView()
view._createContextMenu(qt.QPoint(0, 0))
+ def testSelection_Simple(self):
+ tree = commonh5.File("/foo/bar/1.mock", "w")
+ item = tree.create_group("a/b/c/d")
+ item.create_group("e").create_group("f")
+
+ model = hdf5.Hdf5TreeModel()
+ model.insertH5pyObject(tree)
+ view = hdf5.Hdf5TreeView()
+ view.setModel(model)
+ view.setSelectedH5Node(item)
+
+ selected = list(view.selectedH5Nodes())[0]
+ self.assertIs(item, selected.h5py_object)
+
+ def testSelection_NotFound(self):
+ tree2 = commonh5.File("/foo/bar/2.mock", "w")
+ tree = commonh5.File("/foo/bar/1.mock", "w")
+ item = tree.create_group("a/b/c/d")
+ item.create_group("e").create_group("f")
+
+ model = hdf5.Hdf5TreeModel()
+ model.insertH5pyObject(tree)
+ view = hdf5.Hdf5TreeView()
+ view.setModel(model)
+ view.setSelectedH5Node(tree2)
+
+ selection = list(view.selectedH5Nodes())
+ self.assertEqual(len(selection), 0)
+
+ def testSelection_ManyGroupFromSameFile(self):
+ tree = commonh5.File("/foo/bar/1.mock", "w")
+ group1 = tree.create_group("a1")
+ group2 = tree.create_group("a2")
+ group3 = tree.create_group("a3")
+ group1.create_group("b/c/d")
+ item = group2.create_group("b/c/d")
+ group3.create_group("b/c/d")
+
+ model = hdf5.Hdf5TreeModel()
+ model.insertH5pyObject(group1)
+ model.insertH5pyObject(group2)
+ model.insertH5pyObject(group3)
+ view = hdf5.Hdf5TreeView()
+ view.setModel(model)
+ view.setSelectedH5Node(item)
+
+ selected = list(view.selectedH5Nodes())[0]
+ self.assertIs(item, selected.h5py_object)
+
+ def testSelection_RootFromSubTree(self):
+ tree = commonh5.File("/foo/bar/1.mock", "w")
+ group = tree.create_group("a1")
+ group.create_group("b/c/d")
+
+ model = hdf5.Hdf5TreeModel()
+ model.insertH5pyObject(group)
+ view = hdf5.Hdf5TreeView()
+ view.setModel(model)
+ view.setSelectedH5Node(group)
+
+ selected = list(view.selectedH5Nodes())[0]
+ self.assertIs(group, selected.h5py_object)
+
+ def testSelection_FileFromSubTree(self):
+ tree = commonh5.File("/foo/bar/1.mock", "w")
+ group = tree.create_group("a1")
+ group.create_group("b").create_group("b").create_group("d")
+
+ model = hdf5.Hdf5TreeModel()
+ model.insertH5pyObject(group)
+ view = hdf5.Hdf5TreeView()
+ view.setModel(model)
+ view.setSelectedH5Node(tree)
+
+ selection = list(view.selectedH5Nodes())
+ self.assertEquals(len(selection), 0)
+
+ def testSelection_Tree(self):
+ tree1 = commonh5.File("/foo/bar/1.mock", "w")
+ tree2 = commonh5.File("/foo/bar/2.mock", "w")
+ tree3 = commonh5.File("/foo/bar/3.mock", "w")
+ tree1.create_group("a/b/c")
+ tree2.create_group("a/b/c")
+ tree3.create_group("a/b/c")
+ item = tree2
+
+ model = hdf5.Hdf5TreeModel()
+ model.insertH5pyObject(tree1)
+ model.insertH5pyObject(tree2)
+ model.insertH5pyObject(tree3)
+ view = hdf5.Hdf5TreeView()
+ view.setModel(model)
+ view.setSelectedH5Node(item)
+
+ selected = list(view.selectedH5Nodes())[0]
+ self.assertIs(item, selected.h5py_object)
+
+ def testSelection_RecurssiveLink(self):
+ """
+ Recurssive link selection
+
+ This example is not really working as expected cause commonh5 do not
+ support recurssive links.
+ But item.name == "/a/b" and the result is found.
+ """
+ tree = commonh5.File("/foo/bar/1.mock", "w")
+ group = tree.create_group("a")
+ group.add_node(commonh5.SoftLink("b", "/"))
+
+ item = tree["/a/b/a/b/a/b/a/b/a/b/a/b/a/b/a/b"]
+
+ model = hdf5.Hdf5TreeModel()
+ model.insertH5pyObject(tree)
+ view = hdf5.Hdf5TreeView()
+ view.setModel(model)
+ view.setSelectedH5Node(item)
+
+ selected = list(view.selectedH5Nodes())[0]
+ self.assertEqual(item.name, selected.h5py_object.name)
+
+ def testSelection_SelectNone(self):
+ tree = commonh5.File("/foo/bar/1.mock", "w")
+
+ model = hdf5.Hdf5TreeModel()
+ model.insertH5pyObject(tree)
+ view = hdf5.Hdf5TreeView()
+ view.setModel(model)
+ view.setSelectedH5Node(tree)
+ view.setSelectedH5Node(None)
+
+ selection = list(view.selectedH5Nodes())
+ self.assertEqual(len(selection), 0)
+
def suite():
test_suite = unittest.TestSuite()
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestHdf5TreeModel))
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestNexusSortFilterProxyModel))
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestHdf5))
+ loadTests = unittest.defaultTestLoader.loadTestsFromTestCase
+ test_suite.addTest(loadTests(TestHdf5TreeModel))
+ test_suite.addTest(loadTests(TestNexusSortFilterProxyModel))
+ test_suite.addTest(loadTests(TestHdf5TreeView))
+ test_suite.addTest(loadTests(TestH5Node))
return test_suite
diff --git a/silx/gui/icons.py b/silx/gui/icons.py
index eaf83b8..07654c1 100644
--- a/silx/gui/icons.py
+++ b/silx/gui/icons.py
@@ -29,15 +29,16 @@ Use :func:`getQIcon` to create Qt QIcon from the name identifying an icon.
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "25/04/2017"
+__date__ = "06/09/2017"
+import os
import logging
import weakref
from . import qt
-from silx.resources import resource_filename
+import silx.resources
from silx.utils import weakref as silxweakref
-from silx.utils.decorators import deprecated
+from silx.utils.deprecation import deprecated
_logger = logging.getLogger(__name__)
@@ -192,7 +193,7 @@ class MultiImageAnimatedIcon(AbstractAnimatedIcon):
self.__frames = []
for i in range(100):
try:
- pixmap = getQPixmap("animated/%s-%02d" % (filename, i))
+ pixmap = getQPixmap("%s/%02d" % (filename, i))
except ValueError:
break
icon = qt.QIcon(pixmap)
@@ -258,13 +259,22 @@ def getWaitIcon():
def getAnimatedIcon(name):
- """Create an AbstractAnimatedIcon from a name.
+ """Create an AbstractAnimatedIcon from a resource name.
+
+ The resource name can be prefixed by the name of a resource directory. For
+ example "silx:foo.png" identify the resource "foo.png" from the resource
+ directory "silx".
+
+ If no prefix are specified, the file with be returned from the silx
+ resource directory with a specific path "gui/icons".
+
+ See also :func:`silx.resources.register_resource_directory`.
Try to load a mng or a gif file, then try to load a multi-image animated
icon.
- In Qt5 mng or gif are not used. It does not take care very well of the
- transparency.
+ In Qt5 mng or gif are not used, because the transparency is not very well
+ managed.
:param str name: Name of the icon, in one of the defined icons
in this module.
@@ -302,6 +312,15 @@ def getAnimatedIcon(name):
def getQIcon(name):
"""Create a QIcon from its name.
+ The resource name can be prefixed by the name of a resource directory. For
+ example "silx:foo.png" identify the resource "foo.png" from the resource
+ directory "silx".
+
+ If no prefix are specified, the file with be returned from the silx
+ resource directory with a specific path "gui/icons".
+
+ See also :func:`silx.resources.register_resource_directory`.
+
:param str name: Name of the icon, in one of the defined icons
in this module.
:return: Corresponding QIcon
@@ -319,6 +338,15 @@ def getQIcon(name):
def getQPixmap(name):
"""Create a QPixmap from its name.
+ The resource name can be prefixed by the name of a resource directory. For
+ example "silx:foo.png" identify the resource "foo.png" from the resource
+ directory "silx".
+
+ If no prefix are specified, the file with be returned from the silx
+ resource directory with a specific path "gui/icons".
+
+ See also :func:`silx.resources.register_resource_directory`.
+
:param str name: Name of the icon, in one of the defined icons
in this module.
:return: Corresponding QPixmap
@@ -332,6 +360,15 @@ def getQFile(name):
"""Create a QFile from an icon name. Filename is found
according to supported Qt formats.
+ The resource name can be prefixed by the name of a resource directory. For
+ example "silx:foo.png" identify the resource "foo.png" from the resource
+ directory "silx".
+
+ If no prefix are specified, the file with be returned from the silx
+ resource directory with a specific path "gui/icons".
+
+ See also :func:`silx.resources.register_resource_directory`.
+
:param str name: Name of the icon, in one of the defined icons
in this module.
:return: Corresponding QFile
@@ -353,7 +390,8 @@ def getQFile(name):
for format_ in _supported_formats:
format_ = str(format_)
- filename = resource_filename('gui/icons/%s.%s' % (name, format_))
+ filename = silx.resources._resource_filename('%s.%s' % (name, format_),
+ default_directory=os.path.join('gui', 'icons'))
qfile = qt.QFile(filename)
if qfile.exists():
return qfile
diff --git a/silx/gui/plot/ColorBar.py b/silx/gui/plot/ColorBar.py
index 93e3c36..8f4bde2 100644
--- a/silx/gui/plot/ColorBar.py
+++ b/silx/gui/plot/ColorBar.py
@@ -33,11 +33,8 @@ __date__ = "11/04/2017"
import logging
import numpy
from ._utils import ticklayout
-from ._utils import clipColormapLogRange
-
-
-from .. import qt
-from silx.gui.plot import Colors
+from .. import qt, icons
+from silx.gui.plot import Colormap
_logger = logging.getLogger(__name__)
@@ -66,12 +63,17 @@ class ColorBarWidget(qt.QWidget):
:param parent: See :class:`QWidget`
:param plot: PlotWidget the colorbar is attached to (optional)
- :param str legend: the label to set to the colormap
+ :param str legend: the label to set to the colorbar
"""
def __init__(self, parent=None, plot=None, legend=None):
- super(ColorBarWidget, self).__init__(parent)
+ self._isConnected = False
self._plot = None
+ self._viewAction = None
+ self._colormap = None
+ self._data = None
+
+ super(ColorBarWidget, self).__init__(parent)
self.__buildGUI()
self.setLegend(legend)
@@ -90,8 +92,6 @@ class ColorBarWidget(qt.QWidget):
self.layout().addWidget(self.legend)
self.layout().setSizeConstraint(qt.QLayout.SetMinAndMaxSize)
- self.setSizePolicy(qt.QSizePolicy.Minimum, qt.QSizePolicy.Expanding)
- self.layout().setContentsMargins(0, 0, 0, 0)
def getPlot(self):
"""Returns the :class:`Plot` associated to this widget or None"""
@@ -100,46 +100,75 @@ class ColorBarWidget(qt.QWidget):
def setPlot(self, plot):
"""Associate a plot to the ColorBar
- :param plot: the plot to associate with the colorbar. If None will remove
- any connection with a previous plot.
+ :param plot: the plot to associate with the colorbar.
+ If None will remove any connection with a previous plot.
"""
- # removing previous plot if any
- if self._plot is not None:
- self._plot.sigActiveImageChanged.disconnect(self._activeImageChanged)
-
- # setting the new plot
+ self._disconnectPlot()
self._plot = plot
- if self._plot is not None:
+ self._connectPlot()
+
+ def _disconnectPlot(self):
+ """Disconnect from Plot signals"""
+ if self._plot is not None and self._isConnected:
+ self._isConnected = False
+ self._plot.sigActiveImageChanged.disconnect(
+ self._activeImageChanged)
+ self._plot.sigPlotSignal.disconnect(self._defaultColormapChanged)
+
+ def _connectPlot(self):
+ """Connect to Plot signals"""
+ if self._plot is not None and not self._isConnected:
+ activeImageLegend = self._plot.getActiveImage(just_legend=True)
+ if activeImageLegend is None: # Show plot default colormap
+ self._syncWithDefaultColormap()
+ else: # Show active image colormap
+ self._activeImageChanged(None, activeImageLegend)
self._plot.sigActiveImageChanged.connect(self._activeImageChanged)
- self._activeImageChanged(self._plot.getActiveImage(just_legend=True))
+ self._plot.sigPlotSignal.connect(self._defaultColormapChanged)
+ self._isConnected = True
+
+ def showEvent(self, event):
+ self._connectPlot()
+ if self._viewAction is not None:
+ self._viewAction.setChecked(True)
+
+ def hideEvent(self, event):
+ self._disconnectPlot()
+ if self._viewAction is not None:
+ self._viewAction.setChecked(False)
def getColormap(self):
- """Return the colormap displayed in the colorbar as a dict.
+ """
+
+ :return: the :class:`.Colormap` colormap displayed in the colorbar.
- It returns None if no colormap is set.
- See :class:`silx.gui.plot.Plot` documentation for the description of the colormap
- dict description.
"""
- return self._colormap.copy()
+ return self.getColorScaleBar().getColormap()
- def setColormap(self, colormap):
+ def setColormap(self, colormap, data=None):
"""Set the colormap to be displayed.
- :param dict colormap: The colormap to apply on the ColorBarWidget
+ :param colormap: The colormap to apply on the
+ ColorBarWidget
+ :type colormap: :class:`.Colormap`
+ :param numpy.ndarray data: the data to display, needed if the colormap
+ require an autoscale
"""
+ self._data = data
+ self.getColorScaleBar().setColormap(colormap=colormap,
+ data=data)
+ if self._colormap is not None:
+ self._colormap.sigChanged.disconnect(self._colormapHasChanged)
self._colormap = colormap
- if self._colormap is None:
- return
-
- if self._colormap['normalization'] not in ('log', 'linear'):
- raise ValueError('Wrong normalization %s' % self._colormap['normalization'])
+ if self._colormap is not None:
+ self._colormap.sigChanged.connect(self._colormapHasChanged)
- if self._colormap['normalization'] is 'log':
- if self._colormap['vmin'] < 1. or self._colormap['vmax'] < 1.:
- _logger.warning('Log colormap with bound <= 1: changing bounds.')
- clipColormapLogRange(colormap)
-
- self.getColorScaleBar().setColormap(self._colormap)
+ def _colormapHasChanged(self):
+ """handler of the Colormap.sigChanged signal
+ """
+ assert self._colormap is not None
+ self.setColormap(colormap=self._colormap,
+ data=self._data)
def setLegend(self, legend):
"""Set the legend displayed along the colorbar
@@ -150,7 +179,7 @@ class ColorBarWidget(qt.QWidget):
self.legend.hide()
self.legend.setText("")
else:
- assert(type(legend) is str)
+ assert type(legend) is str
self.legend.show()
self.legend.setText(legend)
@@ -163,10 +192,10 @@ class ColorBarWidget(qt.QWidget):
"""
return self.legend.getText()
- def _activeImageChanged(self, legend):
+ def _activeImageChanged(self, previous, legend):
"""Handle plot active curve changed"""
- if legend is None: # No active image, display default colormap
- self._syncWithDefaultColormap()
+ if legend is None: # No active image, display no colormap
+ self.setColormap(colormap=None)
return
# Sync with active image
@@ -174,32 +203,25 @@ class ColorBarWidget(qt.QWidget):
# RGB(A) image, display default colormap
if image.ndim != 2:
- self._syncWithDefaultColormap()
+ self.setColormap(colormap=None)
return
# data image, sync with image colormap
# do we need the copy here : used in the case we are changing
# vmin and vmax but should have already be done by the plot
- cmap = self._plot.getActiveImage().getColormap().copy()
- if cmap['autoscale']:
- if cmap['normalization'] == 'log':
- data = image[
- numpy.logical_and(image > 0, numpy.isfinite(image))]
- else:
- data = image[numpy.isfinite(image)]
- cmap['vmin'], cmap['vmax'] = data.min(), data.max()
-
- self.setColormap(cmap)
+ self.setColormap(colormap=self._plot.getActiveImage().getColormap(),
+ data=image)
- def _defaultColormapChanged(self):
+ def _defaultColormapChanged(self, event):
"""Handle plot default colormap changed"""
- if self._plot.getActiveImage() is None:
+ if (event['event'] == 'defaultColormapChanged' and
+ self._plot.getActiveImage() is None):
# No active image, take default colormap update into account
self._syncWithDefaultColormap()
- def _syncWithDefaultColormap(self):
+ def _syncWithDefaultColormap(self, data=None):
"""Update colorbar according to plot default colormap"""
- self.setColormap(self._plot.getDefaultColormap())
+ self.setColormap(self._plot.getDefaultColormap(), data)
def getColorScaleBar(self):
"""
@@ -208,6 +230,21 @@ class ColorBarWidget(qt.QWidget):
and ticks"""
return self._colorScale
+ def getToggleViewAction(self):
+ """Returns a checkable action controlling this widget's visibility.
+
+ :rtype: QAction
+ """
+ if self._viewAction is None:
+ self._viewAction = qt.QAction(self)
+ self._viewAction.setText('Colorbar')
+ self._viewAction.setIcon(icons.getQIcon('colorbar'))
+ self._viewAction.setToolTip('Show/Hide the colorbar')
+ self._viewAction.setCheckable(True)
+ self._viewAction.setChecked(self.isVisible())
+ self._viewAction.toggled[bool].connect(self.setVisible)
+ return self._viewAction
+
class _VerticalLegend(qt.QLabel):
"""Display vertically the given text
@@ -251,12 +288,11 @@ class ColorScaleBar(qt.QWidget):
To run the following sample code, a QApplication must be initialized.
- >>> colormap={'name':'gray',
- ... 'normalization':'log',
- ... 'vmin':1,
- ... 'vmax':100000,
- ... 'autoscale':False
- ... }
+ >>> colormap = Colormap(name='gray',
+ ... norm='log',
+ ... vmin=1,
+ ... vmax=100000,
+ ... )
>>> colorscale = ColorScaleBar(parent=None,
... colormap=colormap )
>>> colorscale.show()
@@ -272,15 +308,8 @@ class ColorScaleBar(qt.QWidget):
"""The tick bar need a margin to display all labels at the correct place.
So the ColorScale should have the same margin in order for both to fit"""
- _MIN_LIM_SCI_FORM = -1000
- """Used for the min and max label to know when we should display it under
- the scientific form"""
-
- _MAX_LIM_SCI_FORM = 1000
- """Used for the min and max label to know when we should display it under
- the scientific form"""
-
- def __init__(self, parent=None, colormap=None, displayTicksValues=True):
+ def __init__(self, parent=None, colormap=None, data=None,
+ displayTicksValues=True):
super(ColorScaleBar, self).__init__(parent)
self.minVal = None
@@ -292,33 +321,41 @@ class ColorScaleBar(qt.QWidget):
# create the left side group (ColorScale)
self.colorScale = _ColorScale(colormap=colormap,
- parent=self,
- margin=ColorScaleBar._TEXT_MARGIN)
+ data=data,
+ parent=self,
+ margin=ColorScaleBar._TEXT_MARGIN)
+ if colormap:
+ vmin, vmax = colormap.getColormapRange(data)
+ else:
+ vmin, vmax = Colormap.DEFAULT_MIN_LIN, Colormap.DEFAULT_MAX_LIN
- self.tickbar = _TickBar(vmin=colormap['vmin'] if colormap else 0.0,
- vmax=colormap['vmax'] if colormap else 1.0,
- norm=colormap['normalization'] if colormap else 'linear',
- parent=self,
- displayValues=displayTicksValues,
- margin=ColorScaleBar._TEXT_MARGIN)
+ norm = colormap.getNormalization() if colormap else Colormap.Colormap.LINEAR
+ self.tickbar = _TickBar(vmin=vmin,
+ vmax=vmax,
+ norm=norm,
+ parent=self,
+ displayValues=displayTicksValues,
+ margin=ColorScaleBar._TEXT_MARGIN)
- self.layout().addWidget(self.tickbar, 1, 0)
- self.layout().addWidget(self.colorScale, 1, 1)
+ self.layout().addWidget(self.tickbar, 1, 0, 1, 1, qt.Qt.AlignRight)
+ self.layout().addWidget(self.colorScale, 1, 1, qt.Qt.AlignLeft)
self.layout().setContentsMargins(0, 0, 0, 0)
self.layout().setSpacing(0)
# max label
self._maxLabel = qt.QLabel(str(1.0), parent=self)
- self._maxLabel.setAlignment(qt.Qt.AlignHCenter)
- self._maxLabel.setSizePolicy(qt.QSizePolicy.Minimum, qt.QSizePolicy.Minimum)
- self.layout().addWidget(self._maxLabel, 0, 1)
+ self._maxLabel.setToolTip(str(0.0))
+ self.layout().addWidget(self._maxLabel, 0, 0, 1, 2, qt.Qt.AlignRight)
# min label
self._minLabel = qt.QLabel(str(0.0), parent=self)
- self._minLabel.setAlignment(qt.Qt.AlignHCenter)
- self._minLabel.setSizePolicy(qt.QSizePolicy.Minimum, qt.QSizePolicy.Minimum)
- self.layout().addWidget(self._minLabel, 2, 1)
+ self._minLabel.setToolTip(str(0.0))
+ self.layout().addWidget(self._minLabel, 2, 0, 1, 2, qt.Qt.AlignRight)
+
+ self.layout().setSizeConstraint(qt.QLayout.SetMinAndMaxSize)
+ self.layout().setColumnStretch(0, 1)
+ self.layout().setRowStretch(1, 1)
def getTickBar(self):
"""
@@ -334,19 +371,34 @@ class ColorScaleBar(qt.QWidget):
"""
return self.colorScale
- def setColormap(self, colormap):
+ def getColormap(self):
+ """
+
+ :returns: the colormap.
+ :rtype: :class:`.Colormap`
+ """
+ return self.colorScale.getColormap()
+
+ def setColormap(self, colormap, data=None):
"""Set the new colormap to be displayed
- :param dict colormap: the colormap to set
+ :param Colormap colormap: the colormap to set
+ :param numpy.ndarray data: the data to display, needed if the colormap
+ require an autoscale
"""
- if colormap is not None:
- self.colorScale.setColormap(colormap)
+ self.colorScale.setColormap(colormap, data)
- self.tickbar.update(vmin=colormap['vmin'],
- vmax=colormap['vmax'],
- norm=colormap['normalization'])
+ if colormap is not None:
+ vmin, vmax = colormap.getColormapRange(data)
+ norm = colormap.getNormalization()
+ else:
+ vmin, vmax = None, None
+ norm = None
- self._setMinMaxLabels(colormap['vmin'], colormap['vmax'])
+ self.tickbar.update(vmin=vmin,
+ vmax=vmax,
+ norm=norm)
+ self._setMinMaxLabels(vmin, vmax)
def setMinMaxVisible(self, val=True):
"""Change visibility of the min label and the max label
@@ -359,17 +411,29 @@ class ColorScaleBar(qt.QWidget):
def _updateMinMax(self):
"""Update the min and max label if we are in the case of the
configuration 'minMaxValueOnly'"""
- if self._minLabel is not None and self._maxLabel is not None:
- if self.minVal is not None:
- if ColorScaleBar._MIN_LIM_SCI_FORM <= self.minVal <= ColorScaleBar._MAX_LIM_SCI_FORM:
- self._minLabel.setText(str(self.minVal))
- else:
- self._minLabel.setText("{0:.0e}".format(self.minVal))
- if self.maxVal is not None:
- if ColorScaleBar._MIN_LIM_SCI_FORM <= self.maxVal <= ColorScaleBar._MAX_LIM_SCI_FORM:
- self._maxLabel.setText(str(self.maxVal))
- else:
- self._maxLabel.setText("{0:.0e}".format(self.maxVal))
+ if self.minVal is None:
+ text, tooltip = '', ''
+ else:
+ if self.minVal == 0 or 0 <= numpy.log10(abs(self.minVal)) < 7:
+ text = '%.7g' % self.minVal
+ else:
+ text = '%.2e' % self.minVal
+ tooltip = repr(self.minVal)
+
+ self._minLabel.setText(text)
+ self._minLabel.setToolTip(tooltip)
+
+ if self.maxVal is None:
+ text, tooltip = '', ''
+ else:
+ if self.maxVal == 0 or 0 <= numpy.log10(abs(self.maxVal)) < 7:
+ text = '%.7g' % self.maxVal
+ else:
+ text = '%.2e' % self.maxVal
+ tooltip = repr(self.maxVal)
+
+ self._maxLabel.setText(text)
+ self._maxLabel.setToolTip(tooltip)
def _setMinMaxLabels(self, minVal, maxVal):
"""Change the value of the min and max labels to be displayed.
@@ -400,12 +464,11 @@ class _ColorScale(qt.QWidget):
To run the following sample code, a QApplication must be initialized.
- >>> colormap={'name':'viridis',
- ... 'normalization':'log',
- ... 'vmin':1,
- ... 'vmax':100000,
- ... 'autoscale':False
- ... }
+ >>> colormap = Colormap(name='viridis',
+ ... norm='log',
+ ... vmin=1,
+ ... vmax=100000,
+ ... )
>>> colorscale = ColorScale(parent=None,
... colormap=colormap)
>>> colorscale.show()
@@ -423,83 +486,94 @@ class _ColorScale(qt.QWidget):
_NB_CONTROL_POINTS = 256
- def __init__(self, colormap, parent=None, margin=5):
+ def __init__(self, colormap, parent=None, margin=5, data=None):
qt.QWidget.__init__(self, parent)
- self.colormap = None
- self.setColormap(colormap)
+ self._colormap = None
+ self.margin = margin
+ self.setColormap(colormap, data)
self.setLayout(qt.QVBoxLayout())
- self.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
+ self.setSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Expanding)
# needed to get the mouse event without waiting for button click
self.setMouseTracking(True)
self.setMargin(margin)
self.setContentsMargins(0, 0, 0, 0)
- def setColormap(self, colormap):
+ self.setMinimumHeight(self._NB_CONTROL_POINTS // 2 + 2 * self.margin)
+ self.setFixedWidth(25)
+
+ def setColormap(self, colormap, data=None):
"""Set the new colormap to be displayed
:param dict colormap: the colormap to set
+ :param data: Optional data for which to compute colormap range.
"""
- if colormap is None:
- return
+ self._colormap = colormap
+ self.setEnabled(colormap is not None)
- if colormap['normalization'] not in ('log', 'linear'):
- raise ValueError("Unrecognized normalization, should be 'linear' or 'log'")
+ if colormap is None:
+ self.vmin, self.vmax = None, None
+ else:
+ assert colormap.getNormalization() in Colormap.Colormap.NORMALIZATIONS
+ self.vmin, self.vmax = self._colormap.getColormapRange(data=data)
+ self._updateColorGradient()
+ self.update()
- if colormap['normalization'] is 'log':
- if not (colormap['vmin'] > 0 and colormap['vmax'] > 0):
- raise ValueError('vmin and vmax should be positives')
- self.colormap = colormap
- self._computeColorPoints()
+ def getColormap(self):
+ """Returns the colormap
- def _computeColorPoints(self):
- """Compute the color points for the gradient
+ :rtype: :class:`.Colormap`
"""
- if self.colormap is None:
+ return None if self._colormap is None else self._colormap
+
+ def _updateColorGradient(self):
+ """Compute the color gradient"""
+ colormap = self.getColormap()
+ if colormap is None:
return
- vmin = self.colormap['vmin']
- vmax = self.colormap['vmax']
- steps = (vmax - vmin)/float(_ColorScale._NB_CONTROL_POINTS)
- self.ctrPoints = numpy.arange(vmin, vmax, steps)
- self.colorsCtrPts = Colors.applyColormapToData(self.ctrPoints,
- name=self.colormap['name'],
- normalization='linear',
- autoscale=self.colormap['autoscale'],
- vmin=vmin,
- vmax=vmax)
+ indices = numpy.linspace(0., 1., self._NB_CONTROL_POINTS)
+ colormapDisp = Colormap.Colormap(name=colormap.getName(),
+ normalization=Colormap.Colormap.LINEAR,
+ vmin=None,
+ vmax=None,
+ colors=colormap.getColormapLUT())
+ colors = colormapDisp.applyToData(indices)
+ self._gradient = qt.QLinearGradient(0, 1, 0, 0)
+ self._gradient.setCoordinateMode(qt.QGradient.StretchToDeviceMode)
+ self._gradient.setStops(
+ [(i, qt.QColor(*color)) for i, color in zip(indices, colors)]
+ )
def paintEvent(self, event):
""""""
- qt.QWidget.paintEvent(self, event)
- if self.colormap is None:
- return
-
- vmin = self.colormap['vmin']
- vmax = self.colormap['vmax']
-
painter = qt.QPainter(self)
- gradient = qt.QLinearGradient(0, 0, 0, self.rect().height() - 2*self.margin)
- for iPt, pt in enumerate(self.ctrPoints):
- colormapPosition = 1 - (pt-vmin) / (vmax-vmin)
- assert(colormapPosition >= 0.0)
- assert(colormapPosition <= 1.0)
- gradient.setColorAt(colormapPosition, qt.QColor(*(self.colorsCtrPts[iPt])))
+ if self.getColormap() is not None:
+ painter.setBrush(self._gradient)
+ penColor = self.palette().color(qt.QPalette.Active,
+ qt.QPalette.Foreground)
+ else:
+ penColor = self.palette().color(qt.QPalette.Disabled,
+ qt.QPalette.Foreground)
+ painter.setPen(penColor)
- painter.setBrush(gradient)
- painter.drawRect(
- qt.QRect(0, self.margin, self.width(), self.height() - 2.*self.margin))
+ painter.drawRect(qt.QRect(
+ 0,
+ self.margin,
+ self.width() - 1.,
+ self.height() - 2. * self.margin - 1.))
def mouseMoveEvent(self, event):
- """"""
- self.setToolTip(str(self.getValueFromRelativePosition(self._getRelativePosition(event.y()))))
+ tooltip = str(self.getValueFromRelativePosition(
+ self._getRelativePosition(event.y())))
+ qt.QToolTip.showText(event.globalPos(), tooltip, self)
super(_ColorScale, self).mouseMoveEvent(event)
def _getRelativePosition(self, yPixel):
"""yPixel : pixel position into _ColorScale widget reference
"""
# widgets are bottom-top referencial but we display in top-bottom referential
- return 1 - float(yPixel)/float(self.height() - 2*self.margin)
+ return 1. - (yPixel - self.margin) / float(self.height() - 2 * self.margin)
def getValueFromRelativePosition(self, value):
"""Return the value in the colorMap from a relative position in the
@@ -508,17 +582,22 @@ class _ColorScale(qt.QWidget):
:param value: float value in [0, 1]
:return: the value in [colormap['vmin'], colormap['vmax']]
"""
+ colormap = self.getColormap()
+ if colormap is None:
+ return
+
value = max(0.0, value)
value = min(value, 1.0)
- vmin = self.colormap['vmin']
- vmax = self.colormap['vmax']
- if self.colormap['normalization'] is 'linear':
+
+ vmin = self.vmin
+ vmax = self.vmax
+ if colormap.getNormalization() == Colormap.Colormap.LINEAR:
return vmin + (vmax - vmin) * value
- elif self.colormap['normalization'] is 'log':
+ elif colormap.getNormalization() == Colormap.Colormap.LOGARITHM:
rpos = (numpy.log10(vmax) - numpy.log10(vmin)) * value + numpy.log10(vmin)
return numpy.power(10., rpos)
else:
- err = "normalization type (%s) is not managed by the _ColorScale Widget" % self.colormap['normalization']
+ err = "normalization type (%s) is not managed by the _ColorScale Widget" % colormap['normalization']
raise ValueError(err)
def setMargin(self, margin):
@@ -529,6 +608,7 @@ class _ColorScale(qt.QWidget):
:param int margin: the margin to apply on the top and bottom.
"""
self.margin = margin
+ self.update()
class _TickBar(qt.QWidget):
@@ -536,7 +616,7 @@ class _TickBar(qt.QWidget):
To run the following sample code, a QApplication must be initialized.
- >>> bar = TickBar(1, 1000, norm='log', parent=None, displayValues=True)
+ >>> bar = _TickBar(1, 1000, norm='log', parent=None, displayValues=True)
>>> bar.show()
.. image:: img/tickbar.png
@@ -569,24 +649,19 @@ class _TickBar(qt.QWidget):
def __init__(self, vmin, vmax, norm, parent=None, displayValues=True,
nticks=None, margin=5):
super(_TickBar, self).__init__(parent)
+ self.margin = margin
+ self._nticks = None
+ self.ticks = ()
+ self.subTicks = ()
self._forcedDisplayType = None
self.ticksDensity = _TickBar.DEFAULT_TICK_DENSITY
self._vmin = vmin
self._vmax = vmax
- # TODO : should be grouped into a global function, called by all
- # logScale displayer to make sure we have the same behavior everywhere
- if self._vmin < 1. or self._vmax < 1.:
- _logger.warning(
- 'Log colormap with bound <= 1: changing bounds.')
- self._vmin, self._vmax = 1., 10.
-
self._norm = norm
self.displayValues = displayValues
self.setTicksNumber(nticks)
- self.setMargin(margin)
- self.setLayout(qt.QVBoxLayout())
self.setMargin(margin)
self.setContentsMargins(0, 0, 0, 0)
@@ -597,8 +672,8 @@ class _TickBar(qt.QWidget):
self._resetWidth()
def _resetWidth(self):
- self.width = _TickBar._WIDTH_DISP_VAL if self.displayValues else _TickBar._WIDTH_NO_DISP_VAL
- self.setFixedWidth(self.width)
+ width = self._WIDTH_DISP_VAL if self.displayValues else self._WIDTH_NO_DISP_VAL
+ self.setFixedWidth(width)
def update(self, vmin, vmax, norm):
self._vmin = vmin
@@ -623,7 +698,6 @@ class _TickBar(qt.QWidget):
optimal number of ticks from the tick density.
"""
self._nticks = nticks
- self.ticks = None
self.computeTicks()
qt.QWidget.update(self)
@@ -644,9 +718,13 @@ class _TickBar(qt.QWidget):
if nticks is None:
nticks = self._getOptimalNbTicks()
- if self._norm == 'log':
+ if self._vmin == self._vmax:
+ # No range: no ticks
+ self.ticks = ()
+ self.subTicks = ()
+ elif self._norm == Colormap.Colormap.LOGARITHM:
self._computeTicksLog(nticks)
- elif self._norm == 'linear':
+ elif self._norm == Colormap.Colormap.LINEAR:
self._computeTicksLin(nticks)
else:
err = 'TickBar - Wrong normalization %s' % self._norm
@@ -693,22 +771,19 @@ class _TickBar(qt.QWidget):
painter.setFont(font)
# paint ticks
- if self.ticks is not None:
- for val in self.ticks:
- self._paintTick(val, painter, majorTick=True)
-
- # paint subticks
- for val in self.subTicks:
- self._paintTick(val, painter, majorTick=False)
+ for val in self.ticks:
+ self._paintTick(val, painter, majorTick=True)
- qt.QWidget.paintEvent(self, event)
+ # paint subticks
+ for val in self.subTicks:
+ self._paintTick(val, painter, majorTick=False)
def _getRelativePosition(self, val):
"""Return the relative position of val according to min and max value
"""
- if self._norm == 'linear':
+ if self._norm == Colormap.Colormap.LINEAR:
return 1 - (val - self._vmin) / (self._vmax - self._vmin)
- elif self._norm == 'log':
+ elif self._norm == Colormap.Colormap.LOGARITHM:
return 1 - (numpy.log10(val) - numpy.log10(self._vmin))/(numpy.log10(self._vmax) - numpy.log(self._vmin))
else:
raise ValueError('Norm is not recognized')
@@ -720,7 +795,7 @@ class _TickBar(qt.QWidget):
with a smaller width
"""
fm = qt.QFontMetrics(painter.font())
- viewportHeight = self.rect().height() - self.margin * 2
+ viewportHeight = self.rect().height() - self.margin * 2 - 1
relativePos = self._getRelativePosition(val)
height = viewportHeight * relativePos
height += self.margin
@@ -728,9 +803,9 @@ class _TickBar(qt.QWidget):
if majorTick is False:
lineWidth /= 2
- painter.drawLine(qt.QLine(self.width - lineWidth,
+ painter.drawLine(qt.QLine(self.width() - lineWidth,
height,
- self.width,
+ self.width(),
height))
if self.displayValues and majorTick is True:
@@ -774,7 +849,6 @@ class _TickBar(qt.QWidget):
:param QFont font: the font we want want to use durint the painting
"""
- assert(type(self._vmin) == type(self._vmax))
form = self._getStandardFormat()
fm = qt.QFontMetrics(font)
diff --git a/silx/gui/plot/Colormap.py b/silx/gui/plot/Colormap.py
new file mode 100644
index 0000000..abe8546
--- /dev/null
+++ b/silx/gui/plot/Colormap.py
@@ -0,0 +1,410 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2015-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 module provides the Colormap object
+"""
+
+from __future__ import absolute_import
+
+__authors__ = ["T. Vincent", "H.Payno"]
+__license__ = "MIT"
+__date__ = "05/12/2016"
+
+from silx.gui import qt
+import copy as copy_mdl
+import numpy
+from .matplotlib import Colormap as MPLColormap
+import logging
+from silx.math.combo import min_max
+
+_logger = logging.getLogger(__file__)
+
+DEFAULT_COLORMAPS = (
+ 'gray', 'reversed gray', 'temperature', 'red', 'green', 'blue')
+"""Tuple of supported colormap names."""
+
+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"""
+
+
+class Colormap(qt.QObject):
+ """Description of a colormap
+
+ :param str name: Name of the colormap
+ :param tuple colors: optional, custom colormap.
+ Nx3 or Nx4 numpy array of RGB(A) colors,
+ either uint8 or float in [0, 1].
+ If 'name' is None, then this array is used as the colormap.
+ :param str norm: Normalization: 'linear' (default) or 'log'
+ :param float vmin:
+ Lower bound of the colormap or None for autoscale (default)
+ :param float vmax:
+ Upper bounds of the colormap or None for autoscale (default)
+ """
+
+ LINEAR = 'linear'
+ """constant for linear normalization"""
+
+ LOGARITHM = 'log'
+ """constant for logarithmic normalization"""
+
+ NORMALIZATIONS = (LINEAR, LOGARITHM)
+ """Tuple of managed normalizations"""
+
+ sigChanged = qt.Signal()
+
+ def __init__(self, name='gray', colors=None, normalization=LINEAR, vmin=None, vmax=None):
+ qt.QObject.__init__(self)
+ 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."
+ m += ' Autoscale will be performed.'
+ m = m % (vmin, vmax)
+ _logger.warning(m)
+ vmin = None
+ vmax = None
+
+ self._name = str(name) if name is not None else None
+ self._setColors(colors)
+ 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
+
+ def isAutoscale(self):
+ """Return True if both min and max are in autoscale mode"""
+ return self._vmin is None or self._vmax is None
+
+ def getName(self):
+ """Return the name of the colormap
+ :rtype: str
+ """
+ return self._name
+
+ def _setColors(self, colors):
+ if colors is None:
+ self._colors = None
+ else:
+ self._colors = numpy.array(colors, copy=True)
+
+ def setName(self, name):
+ """Set the name of the colormap and load the colors corresponding to
+ the name
+
+ :param str name: the name of the colormap (should be in ['gray',
+ 'reversed gray', 'temperature', 'red', 'green', 'blue', 'jet',
+ 'viridis', 'magma', 'inferno', 'plasma']
+ """
+ assert name in self.getSupportedColormaps()
+ self._name = str(name)
+ self._colors = None
+ self.sigChanged.emit()
+
+ def getColormapLUT(self):
+ """Return the list of colors for the colormap. None if not setted
+
+ :return: the list of colors for the colormap. None if not setted
+ :rtype: numpy.ndarray
+ """
+ return self._colors
+
+ def setColormapLUT(self, colors):
+ """
+ Set the colors of the colormap.
+
+ :param numpy.ndarray colors: the colors of the LUT
+
+ .. warning: this will set the value of name to an empty string
+ """
+ self._setColors(colors)
+ if len(colors) is 0:
+ self._colors = None
+
+ self._name = None
+ self.sigChanged.emit()
+
+ def getNormalization(self):
+ """Return the normalization of the colormap ('log' or 'linear')
+
+ :return: the normalization of the colormap
+ :rtype: str
+ """
+ return self._normalization
+
+ def setNormalization(self, norm):
+ """Set the norm ('log', 'linear')
+
+ :param str norm: the norm to set
+ """
+ self._normalization = str(norm)
+ self.sigChanged.emit()
+
+ def getVMin(self):
+ """Return the lower bound of the colormap
+
+ :return: the lower bound of the colormap
+ :rtype: float or None
+ """
+ return self._vmin
+
+ def setVMin(self, vmin):
+ """Set the minimal value of the colormap
+
+ :param float vmin: Lower bound of the colormap or None for autoscale
+ (default)
+ value)
+ """
+ if vmin is not None:
+ if self._vmax is not None and vmin >= self._vmax:
+ err = "Can't set vmin because vmin >= vmax."
+ err += "vmin = %s, vmax = %s" %(vmin, self._vmax)
+ raise ValueError(err)
+
+ self._vmin = vmin
+ self.sigChanged.emit()
+
+ def getVMax(self):
+ """Return the upper bounds of the colormap or None
+
+ :return: the upper bounds of the colormap or None
+ :rtype: float or None
+ """
+ return self._vmax
+
+ def setVMax(self, vmax):
+ """Set the maximal value of the colormap
+
+ :param float vmax: Upper bounds of the colormap or None for autoscale
+ (default)
+ """
+ if vmax is not None:
+ if self._vmin is not None and vmax <= self._vmin:
+ err = "Can't set vmax because vmax <= vmin."
+ err += "vmin = %s, vmax = %s" %(self._vmin, vmax)
+ raise ValueError(err)
+
+ self._vmax = vmax
+ self.sigChanged.emit()
+
+ def getColormapRange(self, data=None):
+ """Return (vmin, vmax)
+
+ :return: the tuple vmin, vmax fitting vmin, vmax, normalization and
+ data if any given
+ :rtype: tuple
+ """
+ vmin = self._vmin
+ vmax = self._vmax
+ assert vmin is None or vmax is None or vmin <= vmax # TODO handle this in setters
+
+ if self.getNormalization() == self.LOGARITHM:
+ # Handle negative bounds as autoscale
+ if vmin is not None and (vmin is not None and vmin <= 0.):
+ mess = 'negative vmin, moving to autoscale for lower bound'
+ _logger.warning(mess)
+ vmin = None
+ if vmax is not None and (vmax is not None and vmax <= 0.):
+ mess = 'negative vmax, moving to autoscale for upper bound'
+ _logger.warning(mess)
+ vmax = None
+
+ if vmin is None or vmax is None: # Handle autoscale
+ # Get min/max from data
+ if data is not None:
+ data = numpy.array(data, copy=False)
+ if data.size == 0: # Fallback an array but no data
+ min_, max_ = self._getDefaultMin(), self._getDefaultMax()
+ else:
+ if self.getNormalization() == self.LOGARITHM:
+ result = min_max(data, min_positive=True, finite=True)
+ min_ = result.min_positive # >0 or None
+ max_ = result.maximum # can be <= 0
+ else:
+ min_, max_ = min_max(data, min_positive=False, finite=True)
+
+ # Handle fallback
+ if min_ is None or not numpy.isfinite(min_):
+ min_ = self._getDefaultMin()
+ if max_ is None or not numpy.isfinite(max_):
+ max_ = self._getDefaultMax()
+ else: # Fallback if no data is provided
+ min_, max_ = self._getDefaultMin(), self._getDefaultMax()
+
+ if vmin is None: # Set vmin respecting provided vmax
+ vmin = min_ if vmax is None else min(min_, vmax)
+
+ if vmax is None:
+ vmax = max(max_, vmin) # Handle max_ <= 0 for log scale
+
+ return vmin, vmax
+
+ def setVRange(self, vmin, vmax):
+ """
+ Set bounds to the colormap
+
+ :param vmin: Lower bound of the colormap or None for autoscale
+ (default)
+ :param vmax: Upper bounds of the colormap or None for autoscale
+ (default)
+ """
+ if vmin is not None and vmax is not None:
+ if vmin >= vmax:
+ err = "Can't set vmin and vmax because vmin >= vmax"
+ err += "vmin = %s, vmax = %s" %(vmin, self._vmax)
+ raise ValueError(err)
+
+ self._vmin = vmin
+ self._vmax = vmax
+ self.sigChanged.emit()
+
+ def __getitem__(self, item):
+ if item == 'autoscale':
+ return self.isAutoscale()
+ elif item == 'name':
+ return self.getName()
+ elif item == 'normalization':
+ return self.getNormalization()
+ elif item == 'vmin':
+ return self.getVMin()
+ elif item == 'vmax':
+ return self.getVMax()
+ elif item == 'colors':
+ return self.getColormapLUT()
+ else:
+ raise KeyError(item)
+
+ def _toDict(self):
+ """Return the equivalent colormap as a dictionary
+ (old colormap representation)
+
+ :return: the representation of the Colormap as a dictionary
+ :rtype: dict
+ """
+ return {
+ 'name': self._name,
+ 'colors': copy_mdl.copy(self._colors),
+ 'vmin': self._vmin,
+ 'vmax': self._vmax,
+ 'autoscale': self.isAutoscale(),
+ 'normalization': self._normalization
+ }
+
+ def _setFromDict(self, dic):
+ """Set values to the colormap from a dictionary
+
+ :param dict dic: the colormap as a dictionary
+ """
+ name = dic['name'] if 'name' in dic else None
+ colors = dic['colors'] if 'colors' in dic else None
+ vmin = dic['vmin'] if 'vmin' in dic else None
+ vmax = dic['vmax'] if 'vmax' in dic else None
+ if 'normalization' in dic:
+ normalization = dic['normalization']
+ else:
+ warn = 'Normalization not given in the dictionary, '
+ warn += 'set by default to ' + Colormap.LINEAR
+ _logger.warning(warn)
+ normalization = Colormap.LINEAR
+
+ if name is None and colors is None:
+ err = 'The colormap should have a name defined or a tuple of colors'
+ raise ValueError(err)
+ if normalization not in Colormap.NORMALIZATIONS:
+ err = 'Given normalization is not recoginized (%s)' % normalization
+ raise ValueError(err)
+
+ # If autoscale, then set boundaries to None
+ if dic.get('autoscale', False):
+ vmin, vmax = None, None
+
+ self._name = name
+ self._colors = colors
+ self._vmin = vmin
+ self._vmax = vmax
+ self._autoscale = True if (vmin is None and vmax is None) else False
+ self._normalization = normalization
+
+ self.sigChanged.emit()
+
+ @staticmethod
+ def _fromDict(dic):
+ colormap = Colormap(name="")
+ colormap._setFromDict(dic)
+ return colormap
+
+ def copy(self):
+ """
+
+ :return: a copy of the Colormap object
+ """
+ return Colormap(name=self._name,
+ colors=copy_mdl.copy(self._colors),
+ vmin=self._vmin,
+ vmax=self._vmax,
+ normalization=self._normalization)
+
+ def applyToData(self, data):
+ """Apply the colormap to the data
+
+ :param numpy.ndarray data: The data to convert.
+ """
+ rgbaImage = MPLColormap.applyColormapToData(colormap=self, data=data)
+ return rgbaImage
+
+ @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')
+ :rtype: tuple
+ """
+ maps = MPLColormap.getSupportedColormaps()
+ return DEFAULT_COLORMAPS + maps
+
+ def __str__(self):
+ return str(self._toDict())
+
+ def _getDefaultMin(self):
+ return DEFAULT_MIN_LIN if self._normalization == Colormap.LINEAR else DEFAULT_MIN_LOG
+
+ def _getDefaultMax(self):
+ return DEFAULT_MAX_LIN if self._normalization == Colormap.LINEAR else DEFAULT_MAX_LOG
+
+ def __eq__(self, other):
+ """Compare colormap values and not pointers"""
+ return (self.getName() == other.getName() and
+ self.getNormalization() == other.getNormalization() and
+ self.getVMin() == other.getVMin() and
+ self.getVMax() == other.getVMax() and
+ numpy.array_equal(self.getColormapLUT(), other.getColormapLUT())
+ )
+
diff --git a/silx/gui/plot/ColormapDialog.py b/silx/gui/plot/ColormapDialog.py
index ad1425c..748dd72 100644
--- a/silx/gui/plot/ColormapDialog.py
+++ b/silx/gui/plot/ColormapDialog.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2016 European Synchrotron Radiation Facility
+# Copyright (c) 2004-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
@@ -42,7 +42,7 @@ Create the colormap dialog and set the colormap description and data range:
Get the colormap description (compatible with :class:`Plot`) from the dialog:
>>> cmap = dialog.getColormap()
->>> cmap['name']
+>>> cmap.getName()
'red'
It is also possible to display an histogram of the image in the dialog.
@@ -61,7 +61,7 @@ from __future__ import division
__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
-__date__ = "29/03/2016"
+__date__ = "02/10/2017"
import logging
@@ -69,37 +69,13 @@ import logging
import numpy
from .. import qt
+from .Colormap import Colormap
from . import PlotWidget
-
+from silx.gui.widgets.FloatEdit import FloatEdit
_logger = logging.getLogger(__name__)
-class _FloatEdit(qt.QLineEdit):
- """Field to edit a float value.
-
- :param parent: See :class:`QLineEdit`
- :param float value: The value to set the QLineEdit to.
- """
- def __init__(self, parent=None, value=None):
- qt.QLineEdit.__init__(self, parent)
- self.setValidator(qt.QDoubleValidator())
- self.setAlignment(qt.Qt.AlignRight)
- if value is not None:
- self.setValue(value)
-
- def value(self):
- """Return the QLineEdit current value as a float."""
- return float(self.text())
-
- def setValue(self, value):
- """Set the current value of the LineEdit
-
- :param float value: The value to set the QLineEdit to.
- """
- self.setText('%g' % value)
-
-
class ColormapDialog(qt.QDialog):
"""A QDialog widget to set the colormap.
@@ -107,7 +83,7 @@ class ColormapDialog(qt.QDialog):
:param str title: The QDialog title
"""
- sigColormapChanged = qt.Signal(dict)
+ sigColormapChanged = qt.Signal(Colormap)
"""Signal triggered when the colormap is changed.
It provides a dict describing the colormap to the slot.
@@ -122,10 +98,13 @@ class ColormapDialog(qt.QDialog):
self._dataRange = None
self._minMaxWasEdited = False
- self._colormapList = (
+ colormaps = [
'gray', 'reversed gray',
'temperature', 'red', 'green', 'blue', 'jet',
- 'viridis', 'magma', 'inferno', 'plasma')
+ 'viridis', 'magma', 'inferno', 'plasma']
+ if 'hsv' in Colormap.getSupportedColormaps():
+ colormaps.append('hsv')
+ self._colormapList = tuple(colormaps)
# Make the GUI
vLayout = qt.QVBoxLayout(self)
@@ -172,14 +151,14 @@ class ColormapDialog(qt.QDialog):
formLayout.addRow('Range:', self._rangeAutoscaleButton)
# Min row
- self._minValue = _FloatEdit(value=1.)
+ self._minValue = FloatEdit(parent=self, value=1.)
self._minValue.setEnabled(False)
self._minValue.textEdited.connect(self._minMaxTextEdited)
self._minValue.editingFinished.connect(self._minEditingFinished)
formLayout.addRow('\tMin:', self._minValue)
# Max row
- self._maxValue = _FloatEdit(value=10.)
+ self._maxValue = FloatEdit(parent=self, value=10.)
self._maxValue.setEnabled(False)
self._maxValue.textEdited.connect(self._minMaxTextEdited)
self._maxValue.editingFinished.connect(self._maxEditingFinished)
@@ -214,8 +193,8 @@ class ColormapDialog(qt.QDialog):
"""Init the plot to display the range and the values"""
self._plot = PlotWidget()
self._plot.setDataMargins(yMinMargin=0.125, yMaxMargin=0.125)
- self._plot.setGraphXLabel("Data Values")
- self._plot.setGraphYLabel("")
+ self._plot.getXAxis().setLabel("Data Values")
+ self._plot.getYAxis().setLabel("")
self._plot.setInteractiveMode('select', zoomOnWheel=False)
self._plot.setActiveCurveHandling(False)
self._plot.setMinimumSize(qt.QSize(250, 200))
@@ -392,17 +371,22 @@ class ColormapDialog(qt.QDialog):
self._plotUpdate()
def getColormap(self):
- """Return the colormap description as a dict.
+ """Return the colormap description as a :class:`.Colormap`.
- See :class:`Plot` for documentation on the colormap dict.
"""
isNormLinear = self._normButtonLinear.isChecked()
- colormap = {
- 'name': str(self._comboBoxColormap.currentText()).lower(),
- 'normalization': 'linear' if isNormLinear else 'log',
- 'autoscale': self._rangeAutoscaleButton.isChecked(),
- 'vmin': self._minValue.value(),
- 'vmax': self._maxValue.value()}
+ if self._rangeAutoscaleButton.isChecked():
+ vmin = None
+ vmax = None
+ else:
+ vmin = self._minValue.value()
+ vmax = self._maxValue.value()
+ norm = Colormap.LINEAR if isNormLinear else Colormap.LOGARITHM
+ colormap = Colormap(
+ name=str(self._comboBoxColormap.currentText()).lower(),
+ normalization=norm,
+ vmin=vmin,
+ vmax=vmax)
return colormap
def setColormap(self, name=None, normalization=None,
@@ -423,9 +407,9 @@ class ColormapDialog(qt.QDialog):
self._comboBoxColormap.setCurrentIndex(index)
if normalization is not None:
- assert normalization in ('linear', 'log')
- self._normButtonLinear.setChecked(normalization == 'linear')
- self._normButtonLog.setChecked(normalization == 'log')
+ assert normalization in Colormap.NORMALIZATIONS
+ self._normButtonLinear.setChecked(normalization == Colormap.LINEAR)
+ self._normButtonLog.setChecked(normalization == Colormap.LOGARITHM)
if vmin is not None:
self._minValue.setValue(vmin)
diff --git a/silx/gui/plot/Colors.py b/silx/gui/plot/Colors.py
index 7a3cd97..2d44d4d 100644
--- a/silx/gui/plot/Colors.py
+++ b/silx/gui/plot/Colors.py
@@ -24,20 +24,18 @@
# ###########################################################################*/
"""Color conversion function, color dictionary and colormap tools."""
-__authors__ = ["V.A. Sole", "T. VINCENT"]
+from __future__ import absolute_import
+
+__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
-__date__ = "16/01/2017"
+__date__ = "15/05/2017"
+from silx.utils.deprecation import deprecated
import logging
-
import numpy
-import matplotlib
-import matplotlib.colors
-import matplotlib.cm
-
-from . import MPLColormap
+from .Colormap import Colormap
_logger = logging.getLogger(__name__)
@@ -143,159 +141,7 @@ def cursorColorForColormap(colormapName):
return _COLORMAP_CURSOR_COLORS.get(colormapName, 'black')
-_CMAPS = {} # Store additional colormaps
-
-
-def getMPLColormap(name):
- """Returns matplotlib colormap corresponding to given name
-
- :param str name: The name of the colormap
- :return: The corresponding colormap
- :rtype: matplolib.colors.Colormap
- """
- if not _CMAPS: # Lazy initialization of own colormaps
- cdict = {'red': ((0.0, 0.0, 0.0),
- (1.0, 1.0, 1.0)),
- 'green': ((0.0, 0.0, 0.0),
- (1.0, 0.0, 0.0)),
- 'blue': ((0.0, 0.0, 0.0),
- (1.0, 0.0, 0.0))}
- _CMAPS['red'] = matplotlib.colors.LinearSegmentedColormap(
- 'red', cdict, 256)
-
- cdict = {'red': ((0.0, 0.0, 0.0),
- (1.0, 0.0, 0.0)),
- 'green': ((0.0, 0.0, 0.0),
- (1.0, 1.0, 1.0)),
- 'blue': ((0.0, 0.0, 0.0),
- (1.0, 0.0, 0.0))}
- _CMAPS['green'] = matplotlib.colors.LinearSegmentedColormap(
- 'green', cdict, 256)
-
- cdict = {'red': ((0.0, 0.0, 0.0),
- (1.0, 0.0, 0.0)),
- 'green': ((0.0, 0.0, 0.0),
- (1.0, 0.0, 0.0)),
- 'blue': ((0.0, 0.0, 0.0),
- (1.0, 1.0, 1.0))}
- _CMAPS['blue'] = matplotlib.colors.LinearSegmentedColormap(
- 'blue', cdict, 256)
-
- # Temperature as defined in spslut
- cdict = {'red': ((0.0, 0.0, 0.0),
- (0.5, 0.0, 0.0),
- (0.75, 1.0, 1.0),
- (1.0, 1.0, 1.0)),
- 'green': ((0.0, 0.0, 0.0),
- (0.25, 1.0, 1.0),
- (0.75, 1.0, 1.0),
- (1.0, 0.0, 0.0)),
- 'blue': ((0.0, 1.0, 1.0),
- (0.25, 1.0, 1.0),
- (0.5, 0.0, 0.0),
- (1.0, 0.0, 0.0))}
- # but limited to 256 colors for a faster display (of the colorbar)
- _CMAPS['temperature'] = \
- matplotlib.colors.LinearSegmentedColormap(
- 'temperature', cdict, 256)
-
- # reversed gray
- cdict = {'red': ((0.0, 1.0, 1.0),
- (1.0, 0.0, 0.0)),
- 'green': ((0.0, 1.0, 1.0),
- (1.0, 0.0, 0.0)),
- 'blue': ((0.0, 1.0, 1.0),
- (1.0, 0.0, 0.0))}
-
- _CMAPS['reversed gray'] = \
- matplotlib.colors.LinearSegmentedColormap(
- 'yerg', cdict, 256)
-
- if name in _CMAPS:
- return _CMAPS[name]
- elif hasattr(MPLColormap, name): # viridis and sister colormaps
- return getattr(MPLColormap, name)
- else:
- # matplotlib built-in
- return matplotlib.cm.get_cmap(name)
-
-
-def getMPLScalarMappable(colormap, data=None):
- """Returns matplotlib ScalarMappable corresponding to colormap
-
- :param dict colormap: The colormap to convert
- :param numpy.ndarray data:
- The data on which the colormap is applied.
- If provided, it is used to compute autoscale.
- :return: matplotlib object corresponding to colormap
- :rtype: matplotlib.cm.ScalarMappable
- """
- assert colormap is not None
-
- if colormap['name'] is not None:
- cmap = getMPLColormap(colormap['name'])
-
- else: # No name, use custom colors
- if 'colors' not in colormap:
- raise ValueError(
- 'addImage: colormap no name nor list of colors.')
- colors = numpy.array(colormap['colors'], copy=True)
- assert len(colors.shape) == 2
- assert colors.shape[-1] in (3, 4)
- if colors.dtype == numpy.uint8:
- # Convert to float in [0., 1.]
- colors = colors.astype(numpy.float32) / 255.
- cmap = matplotlib.colors.ListedColormap(colors)
-
- if colormap['normalization'].startswith('log'):
- vmin, vmax = None, None
- if not colormap['autoscale']:
- if colormap['vmin'] > 0.:
- vmin = colormap['vmin']
- if colormap['vmax'] > 0.:
- vmax = colormap['vmax']
-
- if vmin is None or vmax is None:
- _logger.warning('Log colormap with negative bounds, ' +
- 'changing bounds to positive ones.')
- elif vmin > vmax:
- _logger.warning('Colormap bounds are inverted.')
- vmin, vmax = vmax, vmin
-
- # Set unset/negative bounds to positive bounds
- if (vmin is None or vmax is None) and data is not None:
- finiteData = data[numpy.isfinite(data)]
- posData = finiteData[finiteData > 0]
- if vmax is None:
- # 1. as an ultimate fallback
- vmax = posData.max() if posData.size > 0 else 1.
- if vmin is None:
- vmin = posData.min() if posData.size > 0 else vmax
- if vmin > vmax:
- vmin = vmax
-
- norm = matplotlib.colors.LogNorm(vmin, vmax)
-
- else: # Linear normalization
- if colormap['autoscale']:
- if data is None:
- vmin, vmax = None, None
- else:
- finiteData = data[numpy.isfinite(data)]
- vmin = finiteData.min()
- vmax = finiteData.max()
- else:
- vmin = colormap['vmin']
- vmax = colormap['vmax']
- if vmin > vmax:
- _logger.warning('Colormap bounds are inverted.')
- vmin, vmax = vmax, vmin
-
- norm = matplotlib.colors.Normalize(vmin, vmax)
-
- return matplotlib.cm.ScalarMappable(norm=norm, cmap=cmap)
-
-
+@deprecated(replacement='silx.gui.plot.Colormap.applyColormap')
def applyColormapToData(data,
name='gray',
normalization='linear',
@@ -324,36 +170,19 @@ def applyColormapToData(data,
:return: The computed RGBA image
:rtype: numpy.ndarray of uint8
"""
- # Debian 7 specific support
- # No transparent colormap with matplotlib < 1.2.0
- # Add support for transparent colormap for uint8 data with
- # colormap with 256 colors, linear norm, [0, 255] range
- if matplotlib.__version__ < '1.2.0':
- if name is None and colors is not None:
- colors = numpy.array(colors, copy=False)
- if (colors.shape[-1] == 4 and
- not numpy.all(numpy.equal(colors[3], 255))):
- # This is a transparent colormap
- if (colors.shape == (256, 4) and
- normalization == 'linear' and
- not autoscale and
- vmin == 0 and vmax == 255 and
- data.dtype == numpy.uint8):
- # Supported case, convert data to RGBA
- return colors[data.reshape(-1)].reshape(
- data.shape + (4,))
- else:
- _logger.warning(
- 'matplotlib %s does not support transparent '
- 'colormap.', matplotlib.__version__)
-
- colormap = dict(name=name,
- normalization=normalization,
- autoscale=autoscale,
- vmin=vmin,
- vmax=vmax,
- colors=colors)
- scalarMappable = getMPLScalarMappable(colormap, data)
- rgbaImage = scalarMappable.to_rgba(data, bytes=True)
-
- return rgbaImage
+ colormap = Colormap(name=name,
+ normalization=normalization,
+ vmin=vmin,
+ vmax=vmax,
+ colors=colors)
+ return colormap.applyToData(data)
+
+
+@deprecated(replacement='silx.gui.plot.Colormap.getSupportedColormaps')
+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')
+ """
+ return Colormap.getSupportedColormaps()
diff --git a/silx/gui/plot/ComplexImageView.py b/silx/gui/plot/ComplexImageView.py
new file mode 100644
index 0000000..1463293
--- /dev/null
+++ b/silx/gui/plot/ComplexImageView.py
@@ -0,0 +1,670 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2017 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This module provides a widget to view 2D complex data.
+
+The :class:`ComplexImageView` widget is dedicated to visualize a single 2D dataset
+of complex data.
+"""
+
+from __future__ import absolute_import
+
+__authors__ = ["Vincent Favre-Nicolin", "T. Vincent"]
+__license__ = "MIT"
+__date__ = "02/10/2017"
+
+
+import logging
+import numpy
+
+from .. import qt, icons
+from .PlotWindow import Plot2D
+from .Colormap import Colormap
+from . import items
+from silx.gui.widgets.FloatEdit import FloatEdit
+
+_logger = logging.getLogger(__name__)
+
+
+_PHASE_COLORMAP = Colormap(
+ name='hsv',
+ vmin=-numpy.pi,
+ vmax=numpy.pi)
+"""Colormap to use for phase"""
+
+# Complex colormap functions
+
+def _phase2rgb(data):
+ """Creates RGBA image with colour-coded phase.
+
+ :param numpy.ndarray data: The data to convert
+ :return: Array of RGBA colors
+ :rtype: numpy.ndarray
+ """
+ if data.size == 0:
+ return numpy.zeros((0, 0, 4), dtype=numpy.uint8)
+
+ phase = numpy.angle(data)
+ return _PHASE_COLORMAP.applyToData(phase)
+
+
+def _complex2rgbalog(data, amin=0., dlogs=2, smax=None):
+ """Returns RGBA colors: colour-coded phases and log10(amplitude) in alpha.
+
+ :param numpy.ndarray data: the complex data array to convert to RGBA
+ :param float amin: the minimum value for the alpha channel
+ :param float dlogs: amplitude range displayed, in log10 units
+ :param float smax:
+ if specified, all values above max will be displayed with an alpha=1
+ """
+ if data.size == 0:
+ return numpy.zeros((0, 0, 4), dtype=numpy.uint8)
+
+ rgba = _phase2rgb(data)
+ sabs = numpy.absolute(data)
+ if smax is not None:
+ sabs[sabs > smax] = smax
+ a = numpy.log10(sabs + 1e-20)
+ a -= a.max() - dlogs # display dlogs orders of magnitude
+ rgba[..., 3] = 255 * (amin + a / dlogs * (1 - amin) * (a > 0))
+ return rgba
+
+
+def _complex2rgbalin(data, gamma=1.0, smax=None):
+ """Returns RGBA colors: colour-coded phase and linear amplitude in alpha.
+
+ :param numpy.ndarray data:
+ :param float gamma: Optional exponent gamma applied to the amplitude
+ :param float smax:
+ """
+ if data.size == 0:
+ return numpy.zeros((0, 0, 4), dtype=numpy.uint8)
+
+ rgba = _phase2rgb(data)
+ a = numpy.absolute(data)
+ if smax is not None:
+ a[a > smax] = smax
+ a /= a.max()
+ rgba[..., 3] = 255 * a**gamma
+ return rgba
+
+
+# Dedicated plot item
+
+class _ImageComplexData(items.ImageData):
+ """Specific plot item to force colormap when using complex colormap.
+
+ This is returning the specific colormap when displaying
+ colored phase + amplitude.
+ """
+
+ def __init__(self):
+ super(_ImageComplexData, self).__init__()
+ self._readOnlyColormap = False
+ self._mode = 'absolute'
+ self._colormaps = { # Default colormaps for all modes
+ 'absolute': Colormap(),
+ 'phase': _PHASE_COLORMAP.copy(),
+ 'real': Colormap(),
+ 'imaginary': Colormap(),
+ 'amplitude_phase': _PHASE_COLORMAP.copy(),
+ 'log10_amplitude_phase': _PHASE_COLORMAP.copy(),
+ }
+
+ _READ_ONLY_MODES = 'amplitude_phase', 'log10_amplitude_phase'
+ """Modes that requires a read-only colormap."""
+
+ def setVisualizationMode(self, mode):
+ """Set the visualization mode to use.
+
+ :param str mode:
+ """
+ mode = str(mode)
+ assert mode in self._colormaps
+
+ if mode != self._mode:
+ # Save current colormap
+ self._colormaps[self._mode] = self.getColormap()
+ self._mode = mode
+
+ # Set colormap for new mode
+ self.setColormap(self._colormaps[mode])
+
+ def getVisualizationMode(self):
+ """Returns the visualization mode in use."""
+ return self._mode
+
+ def _isReadOnlyColormap(self):
+ """Returns True if colormap should not be modified."""
+ return self.getVisualizationMode() in self._READ_ONLY_MODES
+
+ def setColormap(self, colormap):
+ if not self._isReadOnlyColormap():
+ super(_ImageComplexData, self).setColormap(colormap)
+
+ def getColormap(self):
+ if self._isReadOnlyColormap():
+ return _PHASE_COLORMAP.copy()
+ else:
+ return super(_ImageComplexData, self).getColormap()
+
+
+# Widgets
+
+class _AmplitudeRangeDialog(qt.QDialog):
+ """QDialog asking for the amplitude range to display."""
+
+ sigRangeChanged = qt.Signal(tuple)
+ """Signal emitted when the range has changed.
+
+ It provides the new range as a 2-tuple: (max, delta)
+ """
+
+ def __init__(self,
+ parent=None,
+ amplitudeRange=None,
+ displayedRange=(None, 2)):
+ super(_AmplitudeRangeDialog, self).__init__(parent)
+ self.setWindowTitle('Set Displayed Amplitude Range')
+
+ if amplitudeRange is not None:
+ amplitudeRange = min(amplitudeRange), max(amplitudeRange)
+ self._amplitudeRange = amplitudeRange
+ self._defaultDisplayedRange = displayedRange
+
+ layout = qt.QFormLayout()
+ self.setLayout(layout)
+
+ if self._amplitudeRange is not None:
+ min_, max_ = self._amplitudeRange
+ layout.addRow(
+ qt.QLabel('Data Amplitude Range: [%g, %g]' % (min_, max_)))
+
+ self._maxLineEdit = FloatEdit(parent=self)
+ self._maxLineEdit.validator().setBottom(0.)
+ self._maxLineEdit.setAlignment(qt.Qt.AlignRight)
+
+ self._maxLineEdit.editingFinished.connect(self._rangeUpdated)
+ layout.addRow('Displayed Max.:', self._maxLineEdit)
+
+ self._autoscale = qt.QCheckBox('autoscale')
+ self._autoscale.toggled.connect(self._autoscaleCheckBoxToggled)
+ layout.addRow('', self._autoscale)
+
+ self._deltaLineEdit = FloatEdit(parent=self)
+ self._deltaLineEdit.validator().setBottom(1.)
+ self._deltaLineEdit.setAlignment(qt.Qt.AlignRight)
+ self._deltaLineEdit.editingFinished.connect(self._rangeUpdated)
+ layout.addRow('Displayed delta (log10 unit):', self._deltaLineEdit)
+
+ buttons = qt.QDialogButtonBox(self)
+ buttons.addButton(qt.QDialogButtonBox.Ok)
+ buttons.addButton(qt.QDialogButtonBox.Cancel)
+ buttons.accepted.connect(self.accept)
+ buttons.rejected.connect(self.reject)
+ layout.addRow(buttons)
+
+ # Set dialog from default values
+ self._resetDialogToDefault()
+
+ self.rejected.connect(self._handleRejected)
+
+ def _resetDialogToDefault(self):
+ """Set Widgets of the dialog from range information
+ """
+ max_, delta = self._defaultDisplayedRange
+
+ if max_ is not None: # Not in autoscale
+ displayedMax = max_
+ elif self._amplitudeRange is not None: # Autoscale with data
+ displayedMax = self._amplitudeRange[1]
+ else: # Autoscale without data
+ displayedMax = ''
+ if displayedMax == "":
+ self._maxLineEdit.setText("")
+ else:
+ self._maxLineEdit.setValue(displayedMax)
+ self._maxLineEdit.setEnabled(max_ is not None)
+
+ self._deltaLineEdit.setValue(delta)
+
+ self._autoscale.setChecked(self._defaultDisplayedRange[0] is None)
+
+ def getRangeInfo(self):
+ """Returns the current range as a 2-tuple (max, delta (in log10))"""
+ if self._autoscale.isChecked():
+ max_ = None
+ else:
+ maxStr = self._maxLineEdit.text()
+ max_ = self._maxLineEdit.value() if maxStr else None
+ return max_, self._deltaLineEdit.value() if self._deltaLineEdit.text() else 2
+
+ def _handleRejected(self):
+ """Reset range info to default when rejected"""
+ self._resetDialogToDefault()
+ self._rangeUpdated()
+
+ def _rangeUpdated(self):
+ """Handle QLineEdit editing finised"""
+ self.sigRangeChanged.emit(self.getRangeInfo())
+
+ def _autoscaleCheckBoxToggled(self, checked):
+ """Handle autoscale checkbox state changes"""
+ if checked: # Use default values
+ if self._amplitudeRange is None:
+ max_ = ''
+ else:
+ max_ = self._amplitudeRange[1]
+ if max_ == "":
+ self._maxLineEdit.setText("")
+ else:
+ self._maxLineEdit.setValue(max_)
+ self._maxLineEdit.setEnabled(not checked)
+ self._rangeUpdated()
+
+
+class _ComplexDataToolButton(qt.QToolButton):
+ """QToolButton providing choices of complex data visualization modes
+
+ :param parent: See :class:`QToolButton`
+ :param plot: The :class:`ComplexImageView` to control
+ """
+
+ _MODES = [
+ ('absolute', 'math-amplitude', 'Amplitude'),
+ ('phase', 'math-phase', 'Phase'),
+ ('real', 'math-real', 'Real part'),
+ ('imaginary', 'math-imaginary', 'Imaginary part'),
+ ('amplitude_phase', 'math-phase-color', 'Amplitude and Phase'),
+ ('log10_amplitude_phase', 'math-phase-color-log', 'Log10(Amp.) and Phase')]
+
+ _RANGE_DIALOG_TEXT = 'Set Amplitude Range...'
+
+ def __init__(self, parent=None, plot=None):
+ super(_ComplexDataToolButton, self).__init__(parent=parent)
+
+ assert plot is not None
+ self._plot2DComplex = plot
+
+ menu = qt.QMenu(self)
+ menu.triggered.connect(self._triggered)
+ self.setMenu(menu)
+
+ for _, icon, text in self._MODES:
+ action = qt.QAction(icons.getQIcon(icon), text, self)
+ action.setIconVisibleInMenu(True)
+ menu.addAction(action)
+
+ self._rangeDialogAction = qt.QAction(self)
+ self._rangeDialogAction.setText(self._RANGE_DIALOG_TEXT)
+ menu.addAction(self._rangeDialogAction)
+
+ self.setPopupMode(qt.QToolButton.InstantPopup)
+
+ self._modeChanged(self._plot2DComplex.getVisualizationMode())
+ self._plot2DComplex.sigVisualizationModeChanged.connect(
+ self._modeChanged)
+
+ def _modeChanged(self, mode):
+ """Handle change of visualization modes"""
+ for actionMode, icon, text in self._MODES:
+ if actionMode == mode:
+ self.setIcon(icons.getQIcon(icon))
+ self.setToolTip('Display the ' + text.lower())
+ break
+
+ self._rangeDialogAction.setEnabled(mode == 'log10_amplitude_phase')
+
+ def _triggered(self, action):
+ """Handle triggering of menu actions"""
+ actionText = action.text()
+
+ if actionText == self._RANGE_DIALOG_TEXT: # Show dialog
+ # Get amplitude range
+ data = self._plot2DComplex.getData(copy=False)
+
+ if data.size > 0:
+ absolute = numpy.absolute(data)
+ dataRange = (numpy.nanmin(absolute), numpy.nanmax(absolute))
+ else:
+ dataRange = None
+
+ # Show dialog
+ dialog = _AmplitudeRangeDialog(
+ parent=self,
+ amplitudeRange=dataRange,
+ displayedRange=self._plot2DComplex._getAmplitudeRangeInfo())
+ dialog.sigRangeChanged.connect(self._rangeChanged)
+ dialog.exec_()
+ dialog.sigRangeChanged.disconnect(self._rangeChanged)
+
+ else: # update mode
+ for mode, _, text in self._MODES:
+ if actionText == text:
+ self._plot2DComplex.setVisualizationMode(mode)
+
+ def _rangeChanged(self, range_):
+ """Handle updates of range in the dialog"""
+ self._plot2DComplex._setAmplitudeRangeInfo(*range_)
+
+
+class ComplexImageView(qt.QWidget):
+ """Display an image of complex data and allow to choose the visualization.
+
+ :param parent: See :class:`QMainWindow`
+ """
+
+ sigDataChanged = qt.Signal()
+ """Signal emitted when data has changed."""
+
+ sigVisualizationModeChanged = qt.Signal(str)
+ """Signal emitted when the visualization mode has changed.
+
+ It provides the new visualization mode.
+ """
+
+ def __init__(self, parent=None):
+ super(ComplexImageView, self).__init__(parent)
+ if parent is None:
+ self.setWindowTitle('ComplexImageView')
+
+ self._mode = 'absolute'
+ self._amplitudeRangeInfo = None, 2
+ self._data = numpy.zeros((0, 0), dtype=numpy.complex)
+ self._displayedData = numpy.zeros((0, 0), dtype=numpy.float)
+
+ self._plot2D = Plot2D(self)
+
+ layout = qt.QHBoxLayout(self)
+ layout.setSpacing(0)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.addWidget(self._plot2D)
+ self.setLayout(layout)
+
+ # Create and add image to the plot
+ self._plotImage = _ImageComplexData()
+ self._plotImage._setLegend('__ComplexImageView__complex_image__')
+ self._plotImage.setData(self._displayedData)
+ self._plotImage.setVisualizationMode(self._mode)
+ self._plot2D._add(self._plotImage)
+ self._plot2D.setActiveImage(self._plotImage.getLegend())
+
+ toolBar = qt.QToolBar('Complex', self)
+ toolBar.addWidget(
+ _ComplexDataToolButton(parent=self, plot=self))
+
+ self._plot2D.insertToolBar(self._plot2D.getProfileToolbar(), toolBar)
+
+ def getPlot(self):
+ """Return the PlotWidget displaying the data"""
+ return self._plot2D
+
+ def _convertData(self, data, mode):
+ """Convert complex data according to provided mode.
+
+ :param numpy.ndarray data: The complex data to convert
+ :param str mode: The visualization mode
+ :return: The data corresponding to the mode
+ :rtype: 2D numpy.ndarray of float or RGBA image
+ """
+ if mode == 'absolute':
+ return numpy.absolute(data)
+ elif mode == 'phase':
+ return numpy.angle(data)
+ elif mode == 'real':
+ return numpy.real(data)
+ elif mode == 'imaginary':
+ return numpy.imag(data)
+ elif mode == 'amplitude_phase':
+ return _complex2rgbalin(data)
+ elif mode == 'log10_amplitude_phase':
+ max_, delta = self._getAmplitudeRangeInfo()
+ return _complex2rgbalog(data, dlogs=delta, smax=max_)
+ else:
+ _logger.error(
+ 'Unsupported conversion mode: %s, fallback to absolute',
+ str(mode))
+ return numpy.absolute(data)
+
+ def _updatePlot(self):
+ """Update the image in the plot"""
+
+ mode = self.getVisualizationMode()
+
+ self.getPlot().getColormapAction().setDisabled(
+ mode in ('amplitude_phase', 'log10_amplitude_phase'))
+
+ self._plotImage.setVisualizationMode(mode)
+
+ image = self.getDisplayedData(copy=False)
+ if mode in ('amplitude_phase', 'log10_amplitude_phase'):
+ # Combined view
+ absolute = numpy.absolute(self.getData(copy=False))
+ self._plotImage.setData(
+ absolute, alternative=image, copy=False)
+ else:
+ self._plotImage.setData(
+ image, alternative=None, copy=False)
+
+ def setData(self, data=None, copy=True):
+ """Set the complex data to display.
+
+ :param numpy.ndarray data: 2D complex data
+ :param bool copy: True (default) to copy the data,
+ False to use provided data (do not modify!).
+ """
+ if data is None:
+ data = numpy.zeros((0, 0), dtype=numpy.complex)
+ else:
+ data = numpy.array(data, copy=copy)
+
+ assert data.ndim == 2
+ if data.dtype.kind != 'c': # Convert to complex
+ data = numpy.array(data, dtype=numpy.complex)
+ shape_changed = (self._data.shape != data.shape)
+ self._data = data
+ self._displayedData = self._convertData(
+ data, self.getVisualizationMode())
+
+ self._updatePlot()
+ if shape_changed:
+ self.getPlot().resetZoom()
+
+ self.sigDataChanged.emit()
+
+ def getData(self, copy=True):
+ """Get the currently displayed complex data.
+
+ :param bool copy: True (default) to return a copy of the data,
+ False to return internal data (do not modify!).
+ :return: The complex data array.
+ :rtype: numpy.ndarray of complex with 2 dimensions
+ """
+ return numpy.array(self._data, copy=copy)
+
+ def getDisplayedData(self, copy=True):
+ """Returns the displayed data depending on the visualization mode
+
+ WARNING: The returned data can be a uint8 RGBA image
+
+ :param bool copy: True (default) to return a copy of the data,
+ False to return internal data (do not modify!)
+ :rtype: numpy.ndarray of float with 2 dims or RGBA image (uint8).
+ """
+ return numpy.array(self._displayedData, copy=copy)
+
+ @staticmethod
+ def getSupportedVisualizationModes():
+ """Returns the supported visualization modes.
+
+ Supported visualization modes are:
+
+ - amplitude: The absolute value provided by numpy.absolute
+ - phase: The phase (or argument) provided by numpy.angle
+ - real: Real part
+ - imaginary: Imaginary part
+ - amplitude_phase: Color-coded phase with amplitude as alpha.
+ - log10_amplitude_phase:
+ Color-coded phase with log10(amplitude) as alpha.
+
+ :rtype: tuple of str
+ """
+ return ('absolute',
+ 'phase',
+ 'real',
+ 'imaginary',
+ 'amplitude_phase',
+ 'log10_amplitude_phase')
+
+ def setVisualizationMode(self, mode):
+ """Set the mode of visualization of the complex data.
+
+ See :meth:`getSupportedVisualizationModes` for the list of
+ supported modes.
+
+ :param str mode: The mode to use.
+ """
+ assert mode in self.getSupportedVisualizationModes()
+ if mode != self._mode:
+ self._mode = mode
+ self._displayedData = self._convertData(
+ self.getData(copy=False), mode)
+ self._updatePlot()
+ self.sigVisualizationModeChanged.emit(mode)
+
+ def getVisualizationMode(self):
+ """Get the current visualization mode of the complex data.
+
+ :rtype: str
+ """
+ return self._mode
+
+ def _setAmplitudeRangeInfo(self, max_=None, delta=2):
+ """Set the amplitude range to display for 'log10_amplitude_phase' mode.
+
+ :param max_: Max of the amplitude range.
+ If None it autoscales to data max.
+ :param float delta: Delta range in log10 to display
+ """
+ self._amplitudeRangeInfo = max_, float(delta)
+ mode = self.getVisualizationMode()
+ if mode == 'log10_amplitude_phase':
+ self._displayedData = self._convertData(
+ self.getData(copy=False), mode)
+ self._updatePlot()
+
+ def _getAmplitudeRangeInfo(self):
+ """Returns the amplitude range to use for 'log10_amplitude_phase' mode.
+
+ :return: (max, delta), if max is None, then it autoscales to data max
+ :rtype: 2-tuple"""
+ return self._amplitudeRangeInfo
+
+ # Image item proxy
+
+ def setColormap(self, colormap):
+ """Set the colormap to use for amplitude, phase, real or imaginary.
+
+ WARNING: This colormap is not used when displaying both
+ amplitude and phase.
+
+ :param Colormap colormap: The colormap
+ """
+ self._plotImage.setColormap(colormap)
+
+ def getColormap(self):
+ """Returns the colormap used to display the data.
+
+ :rtype: Colormap
+ """
+ # Returns internal colormap and bypass forcing colormap
+ return items.ImageData.getColormap(self._plotImage)
+
+ def getOrigin(self):
+ """Returns the offset from origin at which to display the image.
+
+ :rtype: 2-tuple of float
+ """
+ return self._plotImage.getOrigin()
+
+ def setOrigin(self, origin):
+ """Set the offset from origin at which to display the image.
+
+ :param origin: (ox, oy) Offset from origin
+ :type origin: float or 2-tuple of float
+ """
+ self._plotImage.setOrigin(origin)
+
+ def getScale(self):
+ """Returns the scale of the image in data coordinates.
+
+ :rtype: 2-tuple of float
+ """
+ return self._plotImage.getScale()
+
+ def setScale(self, scale):
+ """Set the scale of the image
+
+ :param scale: (sx, sy) Scale of the image
+ :type scale: float or 2-tuple of float
+ """
+ self._plotImage.setScale(scale)
+
+ # PlotWidget API proxy
+
+ def getXAxis(self):
+ """Returns the X axis
+
+ :rtype: :class:`.items.Axis`
+ """
+ return self.getPlot().getXAxis()
+
+ def getYAxis(self):
+ """Returns an Y axis
+
+ :rtype: :class:`.items.Axis`
+ """
+ return self.getPlot().getYAxis(axis='left')
+
+ def getGraphTitle(self):
+ """Return the plot main title as a str."""
+ return self.getPlot().getGraphTitle()
+
+ def setGraphTitle(self, title=""):
+ """Set the plot main title.
+
+ :param str title: Main title of the plot (default: '')
+ """
+ self.getPlot().setGraphTitle(title)
+
+ def setKeepDataAspectRatio(self, flag):
+ """Set whether the plot keeps data aspect ratio or not.
+
+ :param bool flag: True to respect data aspect ratio
+ """
+ self.getPlot().setKeepDataAspectRatio(flag)
+
+ def isKeepDataAspectRatio(self):
+ """Returns whether the plot is keeping data aspect ratio or not."""
+ return self.getPlot().isKeepDataAspectRatio()
diff --git a/silx/gui/plot/CurvesROIWidget.py b/silx/gui/plot/CurvesROIWidget.py
index 13c3de0..4b10cd6 100644
--- a/silx/gui/plot/CurvesROIWidget.py
+++ b/silx/gui/plot/CurvesROIWidget.py
@@ -46,7 +46,7 @@ ROI are defined by :
__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
-__date__ = "26/04/2017"
+__date__ = "27/06/2017"
from collections import OrderedDict
@@ -168,9 +168,9 @@ class CurvesROIWidget(qt.QWidget):
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")
+ - ``"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
@@ -194,9 +194,11 @@ class CurvesROIWidget(qt.QWidget):
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")
+ - ``"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.
@@ -742,7 +744,7 @@ class CurvesROIDockWidget(qt.QDockWidget):
"""Handle ROI widget signal"""
_logger.debug("PlotWindow._roiSignal %s", str(ddict))
if ddict['event'] == "AddROI":
- xmin, xmax = self.plot.getGraphXLimits()
+ xmin, xmax = self.plot.getXAxis().getLimits()
fromdata = xmin + 0.25 * (xmax - xmin)
todata = xmin + 0.75 * (xmax - xmin)
self.plot.remove('ROI min', kind='marker')
@@ -786,7 +788,7 @@ class CurvesROIDockWidget(qt.QDockWidget):
if newroi == "ICR":
roiDict[newroi]['type'] = "Default"
else:
- roiDict[newroi]['type'] = self.plot.getGraphXLabel()
+ roiDict[newroi]['type'] = self.plot.getXAxis().getLabel()
roiDict[newroi]['from'] = fromdata
roiDict[newroi]['to'] = todata
self.roiWidget.fillFromROIDict(roilist=roiList,
diff --git a/silx/gui/plot/ImageView.py b/silx/gui/plot/ImageView.py
index 780215e..803a2fc 100644
--- a/silx/gui/plot/ImageView.py
+++ b/silx/gui/plot/ImageView.py
@@ -34,12 +34,7 @@ Basic usage of :class:`ImageView` is through the following methods:
default colormap to use and update the currently displayed image.
- :meth:`ImageView.setImage` to update the displayed image.
-The :class:`ImageView` uses :class:`PlotWindow` and also
-exposes :class:`silx.gui.plot.Plot` API for further control
-(plot title, axes labels, adding other images, ...).
-
-For an example of use, see the implementation of :class:`ImageViewMainWindow`,
-and `example/imageview.py`.
+For an example of use, see `imageview.py` in :ref:`sample-code`.
"""
from __future__ import division
@@ -47,7 +42,7 @@ from __future__ import division
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "13/10/2016"
+__date__ = "17/08/2017"
import logging
@@ -55,7 +50,8 @@ import numpy
from .. import qt
-from . import items, PlotWindow, PlotWidget, PlotActions
+from . import items, PlotWindow, PlotWidget, actions
+from .Colormap import Colormap
from .Colors import cursorColorForColormap
from .PlotTools import LimitsToolBar
from .Profile import ProfileToolBar
@@ -253,9 +249,13 @@ class ImageView(PlotWindow):
Use :meth:`setImage` to control the displayed image.
This class also provides the :class:`silx.gui.plot.Plot` API.
+ The :class:`ImageView` inherits from :class:`.PlotWindow` (which provides
+ the toolbars) and also exposes :class:`.PlotWidget` API for further
+ plot control (plot title, axes labels, aspect ratio, ...).
+
:param parent: The parent of this widget or None.
:param backend: The backend to use for the plot (default: matplotlib).
- See :class:`.Plot` for the list of supported backend.
+ See :class:`.PlotWidget` for the list of supported backend.
:type backend: str or :class:`BackendBase.BackendBase`
"""
@@ -318,7 +318,7 @@ class ImageView(PlotWindow):
self._histoHPlot = PlotWidget(backend=backend)
self._histoHPlot.setInteractiveMode('zoom')
- self._histoHPlot.setCallback(self._histoHPlotCB)
+ self._histoHPlot.sigPlotSignal.connect(self._histoHPlotCB)
self._histoHPlot.getWidgetHandle().sizeHint = sizeHint
self._histoHPlot.getWidgetHandle().minimumSizeHint = sizeHint
@@ -326,39 +326,39 @@ class ImageView(PlotWindow):
self.setInteractiveMode('zoom') # Color set in setColormap
self.sigPlotSignal.connect(self._imagePlotCB)
- self.sigSetYAxisInverted.connect(self._updateYAxisInverted)
+ self.getYAxis().sigInvertedChanged.connect(self._updateYAxisInverted)
self.sigActiveImageChanged.connect(self._activeImageChangedSlot)
self._histoVPlot = PlotWidget(backend=backend)
self._histoVPlot.setInteractiveMode('zoom')
- self._histoVPlot.setCallback(self._histoVPlotCB)
+ self._histoVPlot.sigPlotSignal.connect(self._histoVPlotCB)
self._histoVPlot.getWidgetHandle().sizeHint = sizeHint
self._histoVPlot.getWidgetHandle().minimumSizeHint = sizeHint
self._radarView = RadarView()
self._radarView.visibleRectDragged.connect(self._radarViewCB)
- self._layout = qt.QGridLayout()
- self._layout.addWidget(self.getWidgetHandle(), 0, 0)
- self._layout.addWidget(self._histoVPlot.getWidgetHandle(), 0, 1)
- self._layout.addWidget(self._histoHPlot.getWidgetHandle(), 1, 0)
- self._layout.addWidget(self._radarView, 1, 1)
+ layout = qt.QGridLayout()
+ layout.addWidget(self.getWidgetHandle(), 0, 0)
+ layout.addWidget(self._histoVPlot.getWidgetHandle(), 0, 1)
+ layout.addWidget(self._histoHPlot.getWidgetHandle(), 1, 0)
+ layout.addWidget(self._radarView, 1, 1)
- self._layout.setColumnMinimumWidth(0, self.IMAGE_MIN_SIZE)
- self._layout.setColumnStretch(0, 1)
- self._layout.setColumnMinimumWidth(1, self.HISTOGRAMS_HEIGHT)
- self._layout.setColumnStretch(1, 0)
+ layout.setColumnMinimumWidth(0, self.IMAGE_MIN_SIZE)
+ layout.setColumnStretch(0, 1)
+ layout.setColumnMinimumWidth(1, self.HISTOGRAMS_HEIGHT)
+ layout.setColumnStretch(1, 0)
- self._layout.setRowMinimumHeight(0, self.IMAGE_MIN_SIZE)
- self._layout.setRowStretch(0, 1)
- self._layout.setRowMinimumHeight(1, self.HISTOGRAMS_HEIGHT)
- self._layout.setRowStretch(1, 0)
+ layout.setRowMinimumHeight(0, self.IMAGE_MIN_SIZE)
+ layout.setRowStretch(0, 1)
+ layout.setRowMinimumHeight(1, self.HISTOGRAMS_HEIGHT)
+ layout.setRowStretch(1, 0)
- self._layout.setSpacing(0)
- self._layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(0)
+ layout.setContentsMargins(0, 0, 0, 0)
centralWidget = qt.QWidget()
- centralWidget.setLayout(self._layout)
+ centralWidget.setLayout(layout)
self.setCentralWidget(centralWidget)
def _dirtyCache(self):
@@ -376,8 +376,8 @@ class ImageView(PlotWindow):
scale = activeImage.getScale()
height, width = data.shape
- xMin, xMax = self.getGraphXLimits()
- yMin, yMax = self.getGraphYLimits()
+ xMin, xMax = self.getXAxis().getLimits()
+ yMin, yMax = self.getYAxis().getLimits()
# Convert plot area limits to image coordinates
# and work in image coordinates (i.e., in pixels)
@@ -440,8 +440,8 @@ class ImageView(PlotWindow):
vOffset = 0.1 * (vMax - vMin)
if vOffset == 0.:
vOffset = 1.
- self._histoHPlot.setGraphYLimits(vMin - vOffset,
- vMax + vOffset)
+ self._histoHPlot.getYAxis().setLimits(vMin - vOffset,
+ vMax + vOffset)
coords = numpy.arange(2 * histoVVisibleData.size)
yCoords = (coords + 1) // 2 + subsetYMin
@@ -458,8 +458,8 @@ class ImageView(PlotWindow):
vOffset = 0.1 * (vMax - vMin)
if vOffset == 0.:
vOffset = 1.
- self._histoVPlot.setGraphXLimits(vMin - vOffset,
- vMax + vOffset)
+ self._histoVPlot.getXAxis().setLimits(vMin - vOffset,
+ vMax + vOffset)
else:
self._dirtyCache()
self._histoHPlot.remove(kind='curve')
@@ -472,8 +472,8 @@ class ImageView(PlotWindow):
Takes care of y coordinate conversion.
"""
- xMin, xMax = self.getGraphXLimits()
- yMin, yMax = self.getGraphYLimits()
+ xMin, xMax = self.getXAxis().getLimits()
+ yMin, yMax = self.getYAxis().getLimits()
self._radarView.setVisibleRect(xMin, yMin, xMax - xMin, yMax - yMin)
# Plots event listeners
@@ -499,6 +499,9 @@ class ImageView(PlotWindow):
data[y][x])
elif eventDict['event'] == 'limitsChanged':
+ self._updateHistogramsLimits()
+
+ def _updateHistogramsLimits(self):
# Do not handle histograms limitsChanged while
# updating their limits from here.
self._updatingLimits = True
@@ -506,15 +509,14 @@ class ImageView(PlotWindow):
# Refresh histograms
self._updateHistograms()
- # could use eventDict['xdata'], eventDict['ydata'] instead
- xMin, xMax = self.getGraphXLimits()
- yMin, yMax = self.getGraphYLimits()
+ xMin, xMax = self.getXAxis().getLimits()
+ yMin, yMax = self.getYAxis().getLimits()
# Set horizontal histo limits
- self._histoHPlot.setGraphXLimits(xMin, xMax)
+ self._histoHPlot.getXAxis().setLimits(xMin, xMax)
# Set vertical histo limits
- self._histoVPlot.setGraphYLimits(yMin, yMax)
+ self._histoVPlot.getYAxis().setLimits(yMin, yMax)
self._updateRadarView()
@@ -542,9 +544,9 @@ class ImageView(PlotWindow):
elif eventDict['event'] == 'limitsChanged':
if (not self._updatingLimits and
- eventDict['xdata'] != self.getGraphXLimits()):
+ eventDict['xdata'] != self.getXAxis().getLimits()):
xMin, xMax = eventDict['xdata']
- self.setGraphXLimits(xMin, xMax)
+ self.getXAxis().setLimits(xMin, xMax)
def _histoVPlotCB(self, eventDict):
"""Callback for vertical histogram plot events."""
@@ -568,9 +570,9 @@ class ImageView(PlotWindow):
elif eventDict['event'] == 'limitsChanged':
if (not self._updatingLimits and
- eventDict['ydata'] != self.getGraphYLimits()):
+ eventDict['ydata'] != self.getYAxis().getLimits()):
yMin, yMax = eventDict['ydata']
- self.setGraphYLimits(yMin, yMax)
+ self.getYAxis().setLimits(yMin, yMax)
def _radarViewCB(self, left, top, width, height):
"""Slot for radar view visible rectangle changes."""
@@ -582,9 +584,9 @@ class ImageView(PlotWindow):
"""Sync image, vertical histogram and radar view axis orientation."""
if inverted is None:
# Do not perform this when called from plot signal
- inverted = self.isYAxisInverted()
+ inverted = self.getYAxis().isInverted()
- self._histoVPlot.setYAxisInverted(inverted)
+ self._histoVPlot.getYAxis().setInverted(inverted)
# Use scale to invert radarView
# RadarView default Y direction is from top to bottom
@@ -643,7 +645,7 @@ class ImageView(PlotWindow):
self._radarView.visibleRectDragged.disconnect(self._radarViewCB)
self._radarView = radarView
self._radarView.visibleRectDragged.connect(self._radarViewCB)
- self._layout.addWidget(self._radarView, 1, 1)
+ self.centralWidget().layout().addWidget(self._radarView, 1, 1)
self._updateYAxisInverted()
@@ -693,42 +695,46 @@ class ImageView(PlotWindow):
:param numpy.ndarray colors: Only used if name is None.
Custom colormap colors as Nx3 or Nx4 RGB or RGBA arrays
"""
- cmapDict = self.getDefaultColormap()
+ cmap = self.getDefaultColormap()
+
+ if isinstance(colormap, Colormap):
+ # Replace colormap
+ cmap = colormap
+
+ self.setDefaultColormap(cmap)
+
+ # Update active image colormap
+ activeImage = self.getActiveImage()
+ if isinstance(activeImage, items.ColormapMixIn):
+ activeImage.setColormap(cmap)
- if isinstance(colormap, dict):
+ elif isinstance(colormap, dict):
# Support colormap parameter as a dict
assert normalization is None
assert autoscale is None
assert vmin is None
assert vmax is None
assert colors is None
- for key, value in colormap.items():
- cmapDict[key] = value
+ cmap._setFromDict(colormap)
else:
if colormap is not None:
- cmapDict['name'] = colormap
+ cmap.setName(colormap)
if normalization is not None:
- cmapDict['normalization'] = normalization
- if autoscale is not None:
- cmapDict['autoscale'] = autoscale
- if vmin is not None:
- cmapDict['vmin'] = vmin
- if vmax is not None:
- cmapDict['vmax'] = vmax
+ cmap.setNormalization(normalization)
+ if autoscale:
+ cmap.setVRange(None, None)
+ else:
+ if vmin is not None:
+ cmap.setVMin(vmin)
+ if vmax is not None:
+ cmap.setVMax(vmax)
if colors is not None:
- cmapDict['colors'] = colors
+ cmap.setColormapLUT(colors)
- cursorColor = cursorColorForColormap(cmapDict['name'])
+ cursorColor = cursorColorForColormap(cmap.getName())
self.setInteractiveMode('zoom', color=cursorColor)
- self.setDefaultColormap(cmapDict)
-
- # Update active image colormap
- activeImage = self.getActiveImage()
- if isinstance(activeImage, items.ColormapMixIn):
- activeImage.setColormap(self.getColormap())
-
def setImage(self, image, origin=(0, 0), scale=(1., 1.),
copy=True, reset=True):
"""Set the image to display.
@@ -768,7 +774,7 @@ class ImageView(PlotWindow):
legend=self._imageLegend,
origin=origin, scale=scale,
colormap=self.getColormap(),
- replace=False)
+ replace=False, resetzoom=False)
self.setActiveImage(self._imageLegend)
self._updateHistograms()
@@ -779,6 +785,8 @@ class ImageView(PlotWindow):
if reset:
self.resetZoom()
+ else:
+ self._updateHistogramsLimits()
# ImageViewMainWindow #########################################################
@@ -793,8 +801,8 @@ class ImageViewMainWindow(ImageView):
super(ImageViewMainWindow, self).__init__(parent, backend)
self.setWindowFlags(qt.Qt.Window)
- self.setGraphXLabel('X')
- self.setGraphYLabel('Y')
+ self.getXAxis().setLabel('X')
+ self.getYAxis().setLabel('Y')
self.setGraphTitle('Image')
# Add toolbars and status bar
@@ -814,11 +822,10 @@ class ImageViewMainWindow(ImageView):
menu.addSeparator()
menu.addAction(self.resetZoomAction)
menu.addAction(self.colormapAction)
- menu.addAction(PlotActions.KeepAspectRatioAction(self, self))
- menu.addAction(PlotActions.YAxisInvertedAction(self, self))
+ menu.addAction(actions.control.KeepAspectRatioAction(self, self))
+ menu.addAction(actions.control.YAxisInvertedAction(self, self))
menu = self.menuBar().addMenu('Profile')
- menu.addAction(self.profile.browseAction)
menu.addAction(self.profile.hLineAction)
menu.addAction(self.profile.vLineAction)
menu.addAction(self.profile.lineAction)
diff --git a/silx/gui/plot/ItemsSelectionDialog.py b/silx/gui/plot/ItemsSelectionDialog.py
new file mode 100644
index 0000000..acb287a
--- /dev/null
+++ b/silx/gui/plot/ItemsSelectionDialog.py
@@ -0,0 +1,282 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2017 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This module provides a dialog widget to select plot items.
+
+.. autoclass:: ItemsSelectionDialog
+
+"""
+
+__authors__ = ["P. Knobel"]
+__license__ = "MIT"
+__date__ = "28/06/2017"
+
+import logging
+
+from silx.gui import qt
+from silx.gui.plot.PlotWidget import PlotWidget
+
+_logger = logging.getLogger(__name__)
+
+
+class KindsSelector(qt.QListWidget):
+ """List widget allowing to select plot item kinds
+ ("curve", "scatter", "image"...)
+ """
+ sigSelectedKindsChanged = qt.Signal(list)
+
+ def __init__(self, parent=None, kinds=None):
+ """
+
+ :param parent: Parent QWidget or None
+ :param tuple(str) kinds: Sequence of kinds. If None, the default
+ behavior is to provide a checkbox for all possible item kinds.
+ """
+ qt.QListWidget.__init__(self, parent)
+
+ self.plot_item_kinds = []
+
+ self.setAvailableKinds(kinds if kinds is not None else PlotWidget.ITEM_KINDS)
+
+ self.setSelectionMode(qt.QAbstractItemView.ExtendedSelection)
+ self.selectAll()
+
+ self.itemSelectionChanged.connect(self.emitSigKindsSelectionChanged)
+
+ def emitSigKindsSelectionChanged(self):
+ self.sigSelectedKindsChanged.emit(self.selectedKinds)
+
+ @property
+ def selectedKinds(self):
+ """Tuple of all selected kinds (as strings)."""
+ # check for updates when self.itemSelectionChanged
+ return [item.text() for item in self.selectedItems()]
+
+ def setAvailableKinds(self, kinds):
+ """Set a list of kinds to be displayed.
+
+ :param list[str] kinds: Sequence of kinds
+ """
+ self.plot_item_kinds = kinds
+
+ self.clear()
+ for kind in self.plot_item_kinds:
+ item = qt.QListWidgetItem(self)
+ item.setText(kind)
+ self.addItem(item)
+
+ def selectAll(self):
+ """Select all available kinds."""
+ if self.selectionMode() in [qt.QAbstractItemView.SingleSelection,
+ qt.QAbstractItemView.NoSelection]:
+ raise RuntimeError("selectAll requires a multiple selection mode")
+ for i in range(self.count()):
+ self.item(i).setSelected(True)
+
+
+class PlotItemsSelector(qt.QTableWidget):
+ """Table widget displaying the legend and kind of all
+ plot items corresponding to a list of specified kinds.
+
+ Selected plot items are provided as property :attr:`selectedPlotItems`.
+ You can be warned of selection changes by listening to signal
+ :attr:`itemSelectionChanged`.
+ """
+ def __init__(self, parent=None, plot=None):
+ if plot is None or not isinstance(plot, PlotWidget):
+ raise AttributeError("parameter plot is required")
+ self.plot = plot
+ """:class:`PlotWidget` instance"""
+
+ self.plot_item_kinds = None
+ """List of plot item kinds (strings)"""
+
+ qt.QTableWidget.__init__(self, parent)
+
+ self.setColumnCount(2)
+
+ self.setSelectionBehavior(qt.QTableWidget.SelectRows)
+
+ def _clear(self):
+ self.clear()
+ self.setHorizontalHeaderLabels(["legend", "type"])
+
+ def setAllKindsFilter(self):
+ """Display all kinds of plot items."""
+ self.setKindsFilter(PlotWidget.ITEM_KINDS)
+
+ def setKindsFilter(self, kinds):
+ """Set list of all kinds of plot items to be displayed.
+
+ :param list[str] kinds: Sequence of kinds
+ """
+ if not set(kinds) <= set(PlotWidget.ITEM_KINDS):
+ raise KeyError("Illegal plot item kinds: %s" %
+ set(kinds) - set(PlotWidget.ITEM_KINDS))
+ self.plot_item_kinds = kinds
+
+ self.updatePlotItems()
+
+ def updatePlotItems(self):
+ self._clear()
+
+ nrows = len(self.plot._getItems(kind=self.plot_item_kinds,
+ just_legend=True))
+ self.setRowCount(nrows)
+
+ # respect order of kinds as set in method setKindsFilter
+ i = 0
+ for kind in self.plot_item_kinds:
+ for plot_item in self.plot._getItems(kind=kind):
+ legend_twitem = qt.QTableWidgetItem(plot_item.getLegend())
+ self.setItem(i, 0, legend_twitem)
+
+ kind_twitem = qt.QTableWidgetItem(kind)
+ self.setItem(i, 1, kind_twitem)
+ i += 1
+
+ @property
+ def selectedPlotItems(self):
+ """List of all selected items"""
+ selection_model = self.selectionModel()
+ selected_rows_idx = selection_model.selectedRows()
+ selected_rows = [idx.row() for idx in selected_rows_idx]
+
+ items = []
+ for row in selected_rows:
+ legend = self.item(row, 0).text()
+ kind = self.item(row, 1).text()
+ items.append(self.plot._getItem(kind, legend))
+
+ return items
+
+
+class ItemsSelectionDialog(qt.QDialog):
+ """This widget is a modal dialog allowing to select one or more plot
+ items, in a table displaying their legend and kind.
+
+ Public methods:
+
+ - :meth:`getSelectedItems`
+ - :meth:`setAvailableKinds`
+ - :meth:`setItemsSelectionMode`
+
+ This widget inherits QDialog and therefore implements the usual
+ dialog methods, e.g. :meth:`exec_`.
+
+ A trivial usage example would be::
+
+ isd = ItemsSelectionDialog(plot=my_plot_widget)
+ isd.setItemsSelectionMode(qt.QTableWidget.SingleSelection)
+ result = isd.exec_()
+ if result:
+ for item in isd.getSelectedItems():
+ print(item.getLegend(), type(item))
+ else:
+ print("Selection cancelled")
+ """
+ def __init__(self, parent=None, plot=None):
+ if plot is None or not isinstance(plot, PlotWidget):
+ raise AttributeError("parameter plot is required")
+ qt.QDialog.__init__(self, parent)
+
+ self.setWindowTitle("Plot items selector")
+
+ kind_selector_label = qt.QLabel("Filter item kinds:", self)
+ item_selector_label = qt.QLabel("Select items:", self)
+
+ self.kind_selector = KindsSelector(self)
+ self.kind_selector.setToolTip(
+ "select one or more item kinds to show them in the item list")
+
+ self.item_selector = PlotItemsSelector(self, plot)
+ self.item_selector.setToolTip("select items")
+
+ self.item_selector.setKindsFilter(self.kind_selector.selectedKinds)
+ self.kind_selector.sigSelectedKindsChanged.connect(
+ self.item_selector.setKindsFilter
+ )
+
+ okb = qt.QPushButton("OK", self)
+ okb.clicked.connect(self.accept)
+
+ cancelb = qt.QPushButton("Cancel", self)
+ cancelb.clicked.connect(self.reject)
+
+ layout = qt.QGridLayout(self)
+ layout.addWidget(kind_selector_label, 0, 0)
+ layout.addWidget(item_selector_label, 0, 1)
+ layout.addWidget(self.kind_selector, 1, 0)
+ layout.addWidget(self.item_selector, 1, 1)
+ layout.addWidget(okb, 2, 0)
+ layout.addWidget(cancelb, 2, 1)
+
+ self.setLayout(layout)
+
+ def getSelectedItems(self):
+ """Return a list of selected plot items
+
+ :return: List of selected plot items
+ :rtype: list[silx.gui.plot.items.Item]"""
+ return self.item_selector.selectedPlotItems
+
+ def setAvailableKinds(self, kinds):
+ """Set a list of kinds to be displayed.
+
+ :param list[str] kinds: Sequence of kinds
+ """
+ self.kind_selector.setAvailableKinds(kinds)
+
+ def selectAllKinds(self):
+ self.kind_selector.selectAll()
+
+ def setItemsSelectionMode(self, mode):
+ """Set selection mode for plot item (single item selection,
+ multiple...).
+
+ :param mode: One of :class:`QTableWidget` selection modes
+ """
+ if mode == self.item_selector.SingleSelection:
+ self.item_selector.setToolTip(
+ "Select one item by clicking on it.")
+ elif mode == self.item_selector.MultiSelection:
+ self.item_selector.setToolTip(
+ "Select one or more items by clicking with the left mouse"
+ " button.\nYou can unselect items by clicking them again.\n"
+ "Multiple items can be toggled by dragging the mouse over them.")
+ elif mode == self.item_selector.ExtendedSelection:
+ self.item_selector.setToolTip(
+ "Select one or more items. You can select multiple items "
+ "by keeping the Ctrl key pushed when clicking.\nYou can "
+ "select a range of items by clicking on the first and "
+ "last while keeping the Shift key pushed.")
+ elif mode == self.item_selector.ContiguousSelection:
+ self.item_selector.setToolTip(
+ "Select one item by clicking on it. If you press the Shift"
+ " key while clicking on a second item,\nall items between "
+ "the two will be selected.")
+ elif mode == self.item_selector.NoSelection:
+ raise ValueError("The NoSelection mode is not allowed "
+ "in this context.")
+ self.item_selector.setSelectionMode(mode)
diff --git a/silx/gui/plot/LegendSelector.py b/silx/gui/plot/LegendSelector.py
index 3af9050..31bc3db 100644
--- a/silx/gui/plot/LegendSelector.py
+++ b/silx/gui/plot/LegendSelector.py
@@ -29,7 +29,7 @@ This widget is meant to work with :class:`PlotWindow`.
__authors__ = ["V.A. Sole", "T. Rueter", "T. Vincent"]
__license__ = "MIT"
-__data__ = "28/04/2016"
+__data__ = "08/08/2016"
import logging
@@ -259,6 +259,7 @@ class LegendModel(qt.QAbstractListModel):
legendList = []
self.legendList = []
self.insertLegendList(0, legendList)
+ self._palette = qt.QPalette()
def __getitem__(self, idx):
if idx >= len(self.legendList):
@@ -282,6 +283,7 @@ class LegendModel(qt.QAbstractListModel):
raise IndexError('list index out of range')
item = self.legendList[idx]
+ isActive = item[1].get("active", False)
if role == qt.Qt.DisplayRole:
# Data to be rendered in the form of text
legend = str(item[0])
@@ -295,14 +297,19 @@ class LegendModel(qt.QAbstractListModel):
return alignment
elif role == qt.Qt.BackgroundRole:
# Background color, must be QBrush
- if idx % 2:
+ if isActive:
+ brush = self._palette.brush(qt.QPalette.Normal, qt.QPalette.Highlight)
+ elif idx % 2:
brush = qt.QBrush(qt.QColor(240, 240, 240))
else:
brush = qt.QBrush(qt.Qt.white)
return brush
elif role == qt.Qt.ForegroundRole:
# ForegroundRole color, must be QBrush
- brush = qt.QBrush(qt.Qt.blue)
+ if isActive:
+ brush = self._palette.brush(qt.QPalette.Normal, qt.QPalette.HighlightedText)
+ else:
+ brush = self._palette.brush(qt.QPalette.Normal, qt.QPalette.WindowText)
return brush
elif role == qt.Qt.CheckStateRole:
return bool(item[2]) # item[2] == True
@@ -513,6 +520,7 @@ class LegendListItemWidget(qt.QItemDelegate):
textAlign = modelIndex.data(qt.Qt.TextAlignmentRole)
painter.setBrush(textBrush)
painter.setFont(self.legend.font())
+ painter.setPen(textBrush.color())
painter.drawText(labelRect, textAlign, legendText)
# Draw icon
@@ -614,7 +622,7 @@ class LegendListView(qt.QListView):
self.setSelectionMode(qt.QAbstractItemView.NoSelection)
if model is None:
- model = LegendModel()
+ model = LegendModel(parent=self)
self.setModel(model)
self.setContextMenu(contextMenu)
@@ -1053,15 +1061,18 @@ class LegendsDockWidget(qt.QDockWidget):
# Use active color if curve is active
if legend == self.plot.getActiveCurve(just_legend=True):
color = qt.QColor(self.plot.getActiveCurveColor())
+ isActive = True
else:
color = qt.QColor.fromRgbF(*curve.getColor())
+ isActive = False
curveInfo = {
'color': color,
'linewidth': curve.getLineWidth(),
'linestyle': curve.getLineStyle(),
'symbol': curve.getSymbol(),
- 'selected': not self.plot.isCurveHidden(legend)}
+ 'selected': not self.plot.isCurveHidden(legend),
+ 'active': isActive}
legendList.append((legend, curveInfo))
self._legendWidget.setLegendList(legendList)
diff --git a/silx/gui/plot/LimitsHistory.py b/silx/gui/plot/LimitsHistory.py
new file mode 100644
index 0000000..a323548
--- /dev/null
+++ b/silx/gui/plot/LimitsHistory.py
@@ -0,0 +1,83 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2017 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This module provides handling of :class:`PlotWidget` limits history.
+"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "19/07/2017"
+
+
+from .. import qt
+
+
+class LimitsHistory(qt.QObject):
+ """Class handling history of limits of a :class:`PlotWidget`.
+
+ :param PlotWidget parent: The plot widget this object is bound to.
+ """
+
+ def __init__(self, parent):
+ self._history = []
+ super(LimitsHistory, self).__init__(parent)
+ self.setParent(parent)
+
+ def setParent(self, parent):
+ """See :meth:`QObject.setParent`.
+
+ :param PlotWidget parent: The PlotWidget this object is bound to.
+ """
+ self.clear() # Clear history when changing parent
+ super(LimitsHistory, self).setParent(parent)
+
+ def push(self):
+ """Append current limits to the history."""
+ plot = self.parent()
+ xmin, xmax = plot.getXAxis().getLimits()
+ ymin, ymax = plot.getYAxis(axis='left').getLimits()
+ y2min, y2max = plot.getYAxis(axis='right').getLimits()
+ self._history.append((xmin, xmax, ymin, ymax, y2min, y2max))
+
+ def pop(self):
+ """Restore previously limits stored in the history.
+
+ :return: True if limits were restored, False if history was empty.
+ :rtype: bool
+ """
+ plot = self.parent()
+ if self._history:
+ limits = self._history.pop(-1)
+ plot.setLimits(*limits)
+ return True
+ else:
+ plot.resetZoom()
+ return False
+
+ def clear(self):
+ """Clear stored limits states."""
+ self._history = []
+
+ def __len__(self):
+ return len(self._history)
diff --git a/silx/gui/plot/MPLColormap.py b/silx/gui/plot/MPLColormap.py
deleted file mode 100644
index 49b11d7..0000000
--- a/silx/gui/plot/MPLColormap.py
+++ /dev/null
@@ -1,1062 +0,0 @@
-# New matplotlib colormaps by Nathaniel J. Smith, Stefan van der Walt,
-# and (in the case of viridis) Eric Firing.
-#
-# This file and the colormaps in it are released under the CC0 license /
-# public domain dedication. We would appreciate credit if you use or
-# redistribute these colormaps, but do not impose any legal restrictions.
-#
-# To the extent possible under law, the persons who associated CC0 with
-# mpl-colormaps have waived all copyright and related or neighboring rights
-# to mpl-colormaps.
-#
-# You should have received a copy of the CC0 legalcode along with this
-# work. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
-"""Matplotlib's new colormaps"""
-
-
-from matplotlib.colors import ListedColormap
-
-
-__all__ = ['magma', 'inferno', 'plasma', 'viridis']
-
-_magma_data = [[0.001462, 0.000466, 0.013866],
- [0.002258, 0.001295, 0.018331],
- [0.003279, 0.002305, 0.023708],
- [0.004512, 0.003490, 0.029965],
- [0.005950, 0.004843, 0.037130],
- [0.007588, 0.006356, 0.044973],
- [0.009426, 0.008022, 0.052844],
- [0.011465, 0.009828, 0.060750],
- [0.013708, 0.011771, 0.068667],
- [0.016156, 0.013840, 0.076603],
- [0.018815, 0.016026, 0.084584],
- [0.021692, 0.018320, 0.092610],
- [0.024792, 0.020715, 0.100676],
- [0.028123, 0.023201, 0.108787],
- [0.031696, 0.025765, 0.116965],
- [0.035520, 0.028397, 0.125209],
- [0.039608, 0.031090, 0.133515],
- [0.043830, 0.033830, 0.141886],
- [0.048062, 0.036607, 0.150327],
- [0.052320, 0.039407, 0.158841],
- [0.056615, 0.042160, 0.167446],
- [0.060949, 0.044794, 0.176129],
- [0.065330, 0.047318, 0.184892],
- [0.069764, 0.049726, 0.193735],
- [0.074257, 0.052017, 0.202660],
- [0.078815, 0.054184, 0.211667],
- [0.083446, 0.056225, 0.220755],
- [0.088155, 0.058133, 0.229922],
- [0.092949, 0.059904, 0.239164],
- [0.097833, 0.061531, 0.248477],
- [0.102815, 0.063010, 0.257854],
- [0.107899, 0.064335, 0.267289],
- [0.113094, 0.065492, 0.276784],
- [0.118405, 0.066479, 0.286321],
- [0.123833, 0.067295, 0.295879],
- [0.129380, 0.067935, 0.305443],
- [0.135053, 0.068391, 0.315000],
- [0.140858, 0.068654, 0.324538],
- [0.146785, 0.068738, 0.334011],
- [0.152839, 0.068637, 0.343404],
- [0.159018, 0.068354, 0.352688],
- [0.165308, 0.067911, 0.361816],
- [0.171713, 0.067305, 0.370771],
- [0.178212, 0.066576, 0.379497],
- [0.184801, 0.065732, 0.387973],
- [0.191460, 0.064818, 0.396152],
- [0.198177, 0.063862, 0.404009],
- [0.204935, 0.062907, 0.411514],
- [0.211718, 0.061992, 0.418647],
- [0.218512, 0.061158, 0.425392],
- [0.225302, 0.060445, 0.431742],
- [0.232077, 0.059889, 0.437695],
- [0.238826, 0.059517, 0.443256],
- [0.245543, 0.059352, 0.448436],
- [0.252220, 0.059415, 0.453248],
- [0.258857, 0.059706, 0.457710],
- [0.265447, 0.060237, 0.461840],
- [0.271994, 0.060994, 0.465660],
- [0.278493, 0.061978, 0.469190],
- [0.284951, 0.063168, 0.472451],
- [0.291366, 0.064553, 0.475462],
- [0.297740, 0.066117, 0.478243],
- [0.304081, 0.067835, 0.480812],
- [0.310382, 0.069702, 0.483186],
- [0.316654, 0.071690, 0.485380],
- [0.322899, 0.073782, 0.487408],
- [0.329114, 0.075972, 0.489287],
- [0.335308, 0.078236, 0.491024],
- [0.341482, 0.080564, 0.492631],
- [0.347636, 0.082946, 0.494121],
- [0.353773, 0.085373, 0.495501],
- [0.359898, 0.087831, 0.496778],
- [0.366012, 0.090314, 0.497960],
- [0.372116, 0.092816, 0.499053],
- [0.378211, 0.095332, 0.500067],
- [0.384299, 0.097855, 0.501002],
- [0.390384, 0.100379, 0.501864],
- [0.396467, 0.102902, 0.502658],
- [0.402548, 0.105420, 0.503386],
- [0.408629, 0.107930, 0.504052],
- [0.414709, 0.110431, 0.504662],
- [0.420791, 0.112920, 0.505215],
- [0.426877, 0.115395, 0.505714],
- [0.432967, 0.117855, 0.506160],
- [0.439062, 0.120298, 0.506555],
- [0.445163, 0.122724, 0.506901],
- [0.451271, 0.125132, 0.507198],
- [0.457386, 0.127522, 0.507448],
- [0.463508, 0.129893, 0.507652],
- [0.469640, 0.132245, 0.507809],
- [0.475780, 0.134577, 0.507921],
- [0.481929, 0.136891, 0.507989],
- [0.488088, 0.139186, 0.508011],
- [0.494258, 0.141462, 0.507988],
- [0.500438, 0.143719, 0.507920],
- [0.506629, 0.145958, 0.507806],
- [0.512831, 0.148179, 0.507648],
- [0.519045, 0.150383, 0.507443],
- [0.525270, 0.152569, 0.507192],
- [0.531507, 0.154739, 0.506895],
- [0.537755, 0.156894, 0.506551],
- [0.544015, 0.159033, 0.506159],
- [0.550287, 0.161158, 0.505719],
- [0.556571, 0.163269, 0.505230],
- [0.562866, 0.165368, 0.504692],
- [0.569172, 0.167454, 0.504105],
- [0.575490, 0.169530, 0.503466],
- [0.581819, 0.171596, 0.502777],
- [0.588158, 0.173652, 0.502035],
- [0.594508, 0.175701, 0.501241],
- [0.600868, 0.177743, 0.500394],
- [0.607238, 0.179779, 0.499492],
- [0.613617, 0.181811, 0.498536],
- [0.620005, 0.183840, 0.497524],
- [0.626401, 0.185867, 0.496456],
- [0.632805, 0.187893, 0.495332],
- [0.639216, 0.189921, 0.494150],
- [0.645633, 0.191952, 0.492910],
- [0.652056, 0.193986, 0.491611],
- [0.658483, 0.196027, 0.490253],
- [0.664915, 0.198075, 0.488836],
- [0.671349, 0.200133, 0.487358],
- [0.677786, 0.202203, 0.485819],
- [0.684224, 0.204286, 0.484219],
- [0.690661, 0.206384, 0.482558],
- [0.697098, 0.208501, 0.480835],
- [0.703532, 0.210638, 0.479049],
- [0.709962, 0.212797, 0.477201],
- [0.716387, 0.214982, 0.475290],
- [0.722805, 0.217194, 0.473316],
- [0.729216, 0.219437, 0.471279],
- [0.735616, 0.221713, 0.469180],
- [0.742004, 0.224025, 0.467018],
- [0.748378, 0.226377, 0.464794],
- [0.754737, 0.228772, 0.462509],
- [0.761077, 0.231214, 0.460162],
- [0.767398, 0.233705, 0.457755],
- [0.773695, 0.236249, 0.455289],
- [0.779968, 0.238851, 0.452765],
- [0.786212, 0.241514, 0.450184],
- [0.792427, 0.244242, 0.447543],
- [0.798608, 0.247040, 0.444848],
- [0.804752, 0.249911, 0.442102],
- [0.810855, 0.252861, 0.439305],
- [0.816914, 0.255895, 0.436461],
- [0.822926, 0.259016, 0.433573],
- [0.828886, 0.262229, 0.430644],
- [0.834791, 0.265540, 0.427671],
- [0.840636, 0.268953, 0.424666],
- [0.846416, 0.272473, 0.421631],
- [0.852126, 0.276106, 0.418573],
- [0.857763, 0.279857, 0.415496],
- [0.863320, 0.283729, 0.412403],
- [0.868793, 0.287728, 0.409303],
- [0.874176, 0.291859, 0.406205],
- [0.879464, 0.296125, 0.403118],
- [0.884651, 0.300530, 0.400047],
- [0.889731, 0.305079, 0.397002],
- [0.894700, 0.309773, 0.393995],
- [0.899552, 0.314616, 0.391037],
- [0.904281, 0.319610, 0.388137],
- [0.908884, 0.324755, 0.385308],
- [0.913354, 0.330052, 0.382563],
- [0.917689, 0.335500, 0.379915],
- [0.921884, 0.341098, 0.377376],
- [0.925937, 0.346844, 0.374959],
- [0.929845, 0.352734, 0.372677],
- [0.933606, 0.358764, 0.370541],
- [0.937221, 0.364929, 0.368567],
- [0.940687, 0.371224, 0.366762],
- [0.944006, 0.377643, 0.365136],
- [0.947180, 0.384178, 0.363701],
- [0.950210, 0.390820, 0.362468],
- [0.953099, 0.397563, 0.361438],
- [0.955849, 0.404400, 0.360619],
- [0.958464, 0.411324, 0.360014],
- [0.960949, 0.418323, 0.359630],
- [0.963310, 0.425390, 0.359469],
- [0.965549, 0.432519, 0.359529],
- [0.967671, 0.439703, 0.359810],
- [0.969680, 0.446936, 0.360311],
- [0.971582, 0.454210, 0.361030],
- [0.973381, 0.461520, 0.361965],
- [0.975082, 0.468861, 0.363111],
- [0.976690, 0.476226, 0.364466],
- [0.978210, 0.483612, 0.366025],
- [0.979645, 0.491014, 0.367783],
- [0.981000, 0.498428, 0.369734],
- [0.982279, 0.505851, 0.371874],
- [0.983485, 0.513280, 0.374198],
- [0.984622, 0.520713, 0.376698],
- [0.985693, 0.528148, 0.379371],
- [0.986700, 0.535582, 0.382210],
- [0.987646, 0.543015, 0.385210],
- [0.988533, 0.550446, 0.388365],
- [0.989363, 0.557873, 0.391671],
- [0.990138, 0.565296, 0.395122],
- [0.990871, 0.572706, 0.398714],
- [0.991558, 0.580107, 0.402441],
- [0.992196, 0.587502, 0.406299],
- [0.992785, 0.594891, 0.410283],
- [0.993326, 0.602275, 0.414390],
- [0.993834, 0.609644, 0.418613],
- [0.994309, 0.616999, 0.422950],
- [0.994738, 0.624350, 0.427397],
- [0.995122, 0.631696, 0.431951],
- [0.995480, 0.639027, 0.436607],
- [0.995810, 0.646344, 0.441361],
- [0.996096, 0.653659, 0.446213],
- [0.996341, 0.660969, 0.451160],
- [0.996580, 0.668256, 0.456192],
- [0.996775, 0.675541, 0.461314],
- [0.996925, 0.682828, 0.466526],
- [0.997077, 0.690088, 0.471811],
- [0.997186, 0.697349, 0.477182],
- [0.997254, 0.704611, 0.482635],
- [0.997325, 0.711848, 0.488154],
- [0.997351, 0.719089, 0.493755],
- [0.997351, 0.726324, 0.499428],
- [0.997341, 0.733545, 0.505167],
- [0.997285, 0.740772, 0.510983],
- [0.997228, 0.747981, 0.516859],
- [0.997138, 0.755190, 0.522806],
- [0.997019, 0.762398, 0.528821],
- [0.996898, 0.769591, 0.534892],
- [0.996727, 0.776795, 0.541039],
- [0.996571, 0.783977, 0.547233],
- [0.996369, 0.791167, 0.553499],
- [0.996162, 0.798348, 0.559820],
- [0.995932, 0.805527, 0.566202],
- [0.995680, 0.812706, 0.572645],
- [0.995424, 0.819875, 0.579140],
- [0.995131, 0.827052, 0.585701],
- [0.994851, 0.834213, 0.592307],
- [0.994524, 0.841387, 0.598983],
- [0.994222, 0.848540, 0.605696],
- [0.993866, 0.855711, 0.612482],
- [0.993545, 0.862859, 0.619299],
- [0.993170, 0.870024, 0.626189],
- [0.992831, 0.877168, 0.633109],
- [0.992440, 0.884330, 0.640099],
- [0.992089, 0.891470, 0.647116],
- [0.991688, 0.898627, 0.654202],
- [0.991332, 0.905763, 0.661309],
- [0.990930, 0.912915, 0.668481],
- [0.990570, 0.920049, 0.675675],
- [0.990175, 0.927196, 0.682926],
- [0.989815, 0.934329, 0.690198],
- [0.989434, 0.941470, 0.697519],
- [0.989077, 0.948604, 0.704863],
- [0.988717, 0.955742, 0.712242],
- [0.988367, 0.962878, 0.719649],
- [0.988033, 0.970012, 0.727077],
- [0.987691, 0.977154, 0.734536],
- [0.987387, 0.984288, 0.742002],
- [0.987053, 0.991438, 0.749504]]
-
-_inferno_data = [[0.001462, 0.000466, 0.013866],
- [0.002267, 0.001270, 0.018570],
- [0.003299, 0.002249, 0.024239],
- [0.004547, 0.003392, 0.030909],
- [0.006006, 0.004692, 0.038558],
- [0.007676, 0.006136, 0.046836],
- [0.009561, 0.007713, 0.055143],
- [0.011663, 0.009417, 0.063460],
- [0.013995, 0.011225, 0.071862],
- [0.016561, 0.013136, 0.080282],
- [0.019373, 0.015133, 0.088767],
- [0.022447, 0.017199, 0.097327],
- [0.025793, 0.019331, 0.105930],
- [0.029432, 0.021503, 0.114621],
- [0.033385, 0.023702, 0.123397],
- [0.037668, 0.025921, 0.132232],
- [0.042253, 0.028139, 0.141141],
- [0.046915, 0.030324, 0.150164],
- [0.051644, 0.032474, 0.159254],
- [0.056449, 0.034569, 0.168414],
- [0.061340, 0.036590, 0.177642],
- [0.066331, 0.038504, 0.186962],
- [0.071429, 0.040294, 0.196354],
- [0.076637, 0.041905, 0.205799],
- [0.081962, 0.043328, 0.215289],
- [0.087411, 0.044556, 0.224813],
- [0.092990, 0.045583, 0.234358],
- [0.098702, 0.046402, 0.243904],
- [0.104551, 0.047008, 0.253430],
- [0.110536, 0.047399, 0.262912],
- [0.116656, 0.047574, 0.272321],
- [0.122908, 0.047536, 0.281624],
- [0.129285, 0.047293, 0.290788],
- [0.135778, 0.046856, 0.299776],
- [0.142378, 0.046242, 0.308553],
- [0.149073, 0.045468, 0.317085],
- [0.155850, 0.044559, 0.325338],
- [0.162689, 0.043554, 0.333277],
- [0.169575, 0.042489, 0.340874],
- [0.176493, 0.041402, 0.348111],
- [0.183429, 0.040329, 0.354971],
- [0.190367, 0.039309, 0.361447],
- [0.197297, 0.038400, 0.367535],
- [0.204209, 0.037632, 0.373238],
- [0.211095, 0.037030, 0.378563],
- [0.217949, 0.036615, 0.383522],
- [0.224763, 0.036405, 0.388129],
- [0.231538, 0.036405, 0.392400],
- [0.238273, 0.036621, 0.396353],
- [0.244967, 0.037055, 0.400007],
- [0.251620, 0.037705, 0.403378],
- [0.258234, 0.038571, 0.406485],
- [0.264810, 0.039647, 0.409345],
- [0.271347, 0.040922, 0.411976],
- [0.277850, 0.042353, 0.414392],
- [0.284321, 0.043933, 0.416608],
- [0.290763, 0.045644, 0.418637],
- [0.297178, 0.047470, 0.420491],
- [0.303568, 0.049396, 0.422182],
- [0.309935, 0.051407, 0.423721],
- [0.316282, 0.053490, 0.425116],
- [0.322610, 0.055634, 0.426377],
- [0.328921, 0.057827, 0.427511],
- [0.335217, 0.060060, 0.428524],
- [0.341500, 0.062325, 0.429425],
- [0.347771, 0.064616, 0.430217],
- [0.354032, 0.066925, 0.430906],
- [0.360284, 0.069247, 0.431497],
- [0.366529, 0.071579, 0.431994],
- [0.372768, 0.073915, 0.432400],
- [0.379001, 0.076253, 0.432719],
- [0.385228, 0.078591, 0.432955],
- [0.391453, 0.080927, 0.433109],
- [0.397674, 0.083257, 0.433183],
- [0.403894, 0.085580, 0.433179],
- [0.410113, 0.087896, 0.433098],
- [0.416331, 0.090203, 0.432943],
- [0.422549, 0.092501, 0.432714],
- [0.428768, 0.094790, 0.432412],
- [0.434987, 0.097069, 0.432039],
- [0.441207, 0.099338, 0.431594],
- [0.447428, 0.101597, 0.431080],
- [0.453651, 0.103848, 0.430498],
- [0.459875, 0.106089, 0.429846],
- [0.466100, 0.108322, 0.429125],
- [0.472328, 0.110547, 0.428334],
- [0.478558, 0.112764, 0.427475],
- [0.484789, 0.114974, 0.426548],
- [0.491022, 0.117179, 0.425552],
- [0.497257, 0.119379, 0.424488],
- [0.503493, 0.121575, 0.423356],
- [0.509730, 0.123769, 0.422156],
- [0.515967, 0.125960, 0.420887],
- [0.522206, 0.128150, 0.419549],
- [0.528444, 0.130341, 0.418142],
- [0.534683, 0.132534, 0.416667],
- [0.540920, 0.134729, 0.415123],
- [0.547157, 0.136929, 0.413511],
- [0.553392, 0.139134, 0.411829],
- [0.559624, 0.141346, 0.410078],
- [0.565854, 0.143567, 0.408258],
- [0.572081, 0.145797, 0.406369],
- [0.578304, 0.148039, 0.404411],
- [0.584521, 0.150294, 0.402385],
- [0.590734, 0.152563, 0.400290],
- [0.596940, 0.154848, 0.398125],
- [0.603139, 0.157151, 0.395891],
- [0.609330, 0.159474, 0.393589],
- [0.615513, 0.161817, 0.391219],
- [0.621685, 0.164184, 0.388781],
- [0.627847, 0.166575, 0.386276],
- [0.633998, 0.168992, 0.383704],
- [0.640135, 0.171438, 0.381065],
- [0.646260, 0.173914, 0.378359],
- [0.652369, 0.176421, 0.375586],
- [0.658463, 0.178962, 0.372748],
- [0.664540, 0.181539, 0.369846],
- [0.670599, 0.184153, 0.366879],
- [0.676638, 0.186807, 0.363849],
- [0.682656, 0.189501, 0.360757],
- [0.688653, 0.192239, 0.357603],
- [0.694627, 0.195021, 0.354388],
- [0.700576, 0.197851, 0.351113],
- [0.706500, 0.200728, 0.347777],
- [0.712396, 0.203656, 0.344383],
- [0.718264, 0.206636, 0.340931],
- [0.724103, 0.209670, 0.337424],
- [0.729909, 0.212759, 0.333861],
- [0.735683, 0.215906, 0.330245],
- [0.741423, 0.219112, 0.326576],
- [0.747127, 0.222378, 0.322856],
- [0.752794, 0.225706, 0.319085],
- [0.758422, 0.229097, 0.315266],
- [0.764010, 0.232554, 0.311399],
- [0.769556, 0.236077, 0.307485],
- [0.775059, 0.239667, 0.303526],
- [0.780517, 0.243327, 0.299523],
- [0.785929, 0.247056, 0.295477],
- [0.791293, 0.250856, 0.291390],
- [0.796607, 0.254728, 0.287264],
- [0.801871, 0.258674, 0.283099],
- [0.807082, 0.262692, 0.278898],
- [0.812239, 0.266786, 0.274661],
- [0.817341, 0.270954, 0.270390],
- [0.822386, 0.275197, 0.266085],
- [0.827372, 0.279517, 0.261750],
- [0.832299, 0.283913, 0.257383],
- [0.837165, 0.288385, 0.252988],
- [0.841969, 0.292933, 0.248564],
- [0.846709, 0.297559, 0.244113],
- [0.851384, 0.302260, 0.239636],
- [0.855992, 0.307038, 0.235133],
- [0.860533, 0.311892, 0.230606],
- [0.865006, 0.316822, 0.226055],
- [0.869409, 0.321827, 0.221482],
- [0.873741, 0.326906, 0.216886],
- [0.878001, 0.332060, 0.212268],
- [0.882188, 0.337287, 0.207628],
- [0.886302, 0.342586, 0.202968],
- [0.890341, 0.347957, 0.198286],
- [0.894305, 0.353399, 0.193584],
- [0.898192, 0.358911, 0.188860],
- [0.902003, 0.364492, 0.184116],
- [0.905735, 0.370140, 0.179350],
- [0.909390, 0.375856, 0.174563],
- [0.912966, 0.381636, 0.169755],
- [0.916462, 0.387481, 0.164924],
- [0.919879, 0.393389, 0.160070],
- [0.923215, 0.399359, 0.155193],
- [0.926470, 0.405389, 0.150292],
- [0.929644, 0.411479, 0.145367],
- [0.932737, 0.417627, 0.140417],
- [0.935747, 0.423831, 0.135440],
- [0.938675, 0.430091, 0.130438],
- [0.941521, 0.436405, 0.125409],
- [0.944285, 0.442772, 0.120354],
- [0.946965, 0.449191, 0.115272],
- [0.949562, 0.455660, 0.110164],
- [0.952075, 0.462178, 0.105031],
- [0.954506, 0.468744, 0.099874],
- [0.956852, 0.475356, 0.094695],
- [0.959114, 0.482014, 0.089499],
- [0.961293, 0.488716, 0.084289],
- [0.963387, 0.495462, 0.079073],
- [0.965397, 0.502249, 0.073859],
- [0.967322, 0.509078, 0.068659],
- [0.969163, 0.515946, 0.063488],
- [0.970919, 0.522853, 0.058367],
- [0.972590, 0.529798, 0.053324],
- [0.974176, 0.536780, 0.048392],
- [0.975677, 0.543798, 0.043618],
- [0.977092, 0.550850, 0.039050],
- [0.978422, 0.557937, 0.034931],
- [0.979666, 0.565057, 0.031409],
- [0.980824, 0.572209, 0.028508],
- [0.981895, 0.579392, 0.026250],
- [0.982881, 0.586606, 0.024661],
- [0.983779, 0.593849, 0.023770],
- [0.984591, 0.601122, 0.023606],
- [0.985315, 0.608422, 0.024202],
- [0.985952, 0.615750, 0.025592],
- [0.986502, 0.623105, 0.027814],
- [0.986964, 0.630485, 0.030908],
- [0.987337, 0.637890, 0.034916],
- [0.987622, 0.645320, 0.039886],
- [0.987819, 0.652773, 0.045581],
- [0.987926, 0.660250, 0.051750],
- [0.987945, 0.667748, 0.058329],
- [0.987874, 0.675267, 0.065257],
- [0.987714, 0.682807, 0.072489],
- [0.987464, 0.690366, 0.079990],
- [0.987124, 0.697944, 0.087731],
- [0.986694, 0.705540, 0.095694],
- [0.986175, 0.713153, 0.103863],
- [0.985566, 0.720782, 0.112229],
- [0.984865, 0.728427, 0.120785],
- [0.984075, 0.736087, 0.129527],
- [0.983196, 0.743758, 0.138453],
- [0.982228, 0.751442, 0.147565],
- [0.981173, 0.759135, 0.156863],
- [0.980032, 0.766837, 0.166353],
- [0.978806, 0.774545, 0.176037],
- [0.977497, 0.782258, 0.185923],
- [0.976108, 0.789974, 0.196018],
- [0.974638, 0.797692, 0.206332],
- [0.973088, 0.805409, 0.216877],
- [0.971468, 0.813122, 0.227658],
- [0.969783, 0.820825, 0.238686],
- [0.968041, 0.828515, 0.249972],
- [0.966243, 0.836191, 0.261534],
- [0.964394, 0.843848, 0.273391],
- [0.962517, 0.851476, 0.285546],
- [0.960626, 0.859069, 0.298010],
- [0.958720, 0.866624, 0.310820],
- [0.956834, 0.874129, 0.323974],
- [0.954997, 0.881569, 0.337475],
- [0.953215, 0.888942, 0.351369],
- [0.951546, 0.896226, 0.365627],
- [0.950018, 0.903409, 0.380271],
- [0.948683, 0.910473, 0.395289],
- [0.947594, 0.917399, 0.410665],
- [0.946809, 0.924168, 0.426373],
- [0.946392, 0.930761, 0.442367],
- [0.946403, 0.937159, 0.458592],
- [0.946903, 0.943348, 0.474970],
- [0.947937, 0.949318, 0.491426],
- [0.949545, 0.955063, 0.507860],
- [0.951740, 0.960587, 0.524203],
- [0.954529, 0.965896, 0.540361],
- [0.957896, 0.971003, 0.556275],
- [0.961812, 0.975924, 0.571925],
- [0.966249, 0.980678, 0.587206],
- [0.971162, 0.985282, 0.602154],
- [0.976511, 0.989753, 0.616760],
- [0.982257, 0.994109, 0.631017],
- [0.988362, 0.998364, 0.644924]]
-
-_plasma_data = [[0.050383, 0.029803, 0.527975],
- [0.063536, 0.028426, 0.533124],
- [0.075353, 0.027206, 0.538007],
- [0.086222, 0.026125, 0.542658],
- [0.096379, 0.025165, 0.547103],
- [0.105980, 0.024309, 0.551368],
- [0.115124, 0.023556, 0.555468],
- [0.123903, 0.022878, 0.559423],
- [0.132381, 0.022258, 0.563250],
- [0.140603, 0.021687, 0.566959],
- [0.148607, 0.021154, 0.570562],
- [0.156421, 0.020651, 0.574065],
- [0.164070, 0.020171, 0.577478],
- [0.171574, 0.019706, 0.580806],
- [0.178950, 0.019252, 0.584054],
- [0.186213, 0.018803, 0.587228],
- [0.193374, 0.018354, 0.590330],
- [0.200445, 0.017902, 0.593364],
- [0.207435, 0.017442, 0.596333],
- [0.214350, 0.016973, 0.599239],
- [0.221197, 0.016497, 0.602083],
- [0.227983, 0.016007, 0.604867],
- [0.234715, 0.015502, 0.607592],
- [0.241396, 0.014979, 0.610259],
- [0.248032, 0.014439, 0.612868],
- [0.254627, 0.013882, 0.615419],
- [0.261183, 0.013308, 0.617911],
- [0.267703, 0.012716, 0.620346],
- [0.274191, 0.012109, 0.622722],
- [0.280648, 0.011488, 0.625038],
- [0.287076, 0.010855, 0.627295],
- [0.293478, 0.010213, 0.629490],
- [0.299855, 0.009561, 0.631624],
- [0.306210, 0.008902, 0.633694],
- [0.312543, 0.008239, 0.635700],
- [0.318856, 0.007576, 0.637640],
- [0.325150, 0.006915, 0.639512],
- [0.331426, 0.006261, 0.641316],
- [0.337683, 0.005618, 0.643049],
- [0.343925, 0.004991, 0.644710],
- [0.350150, 0.004382, 0.646298],
- [0.356359, 0.003798, 0.647810],
- [0.362553, 0.003243, 0.649245],
- [0.368733, 0.002724, 0.650601],
- [0.374897, 0.002245, 0.651876],
- [0.381047, 0.001814, 0.653068],
- [0.387183, 0.001434, 0.654177],
- [0.393304, 0.001114, 0.655199],
- [0.399411, 0.000859, 0.656133],
- [0.405503, 0.000678, 0.656977],
- [0.411580, 0.000577, 0.657730],
- [0.417642, 0.000564, 0.658390],
- [0.423689, 0.000646, 0.658956],
- [0.429719, 0.000831, 0.659425],
- [0.435734, 0.001127, 0.659797],
- [0.441732, 0.001540, 0.660069],
- [0.447714, 0.002080, 0.660240],
- [0.453677, 0.002755, 0.660310],
- [0.459623, 0.003574, 0.660277],
- [0.465550, 0.004545, 0.660139],
- [0.471457, 0.005678, 0.659897],
- [0.477344, 0.006980, 0.659549],
- [0.483210, 0.008460, 0.659095],
- [0.489055, 0.010127, 0.658534],
- [0.494877, 0.011990, 0.657865],
- [0.500678, 0.014055, 0.657088],
- [0.506454, 0.016333, 0.656202],
- [0.512206, 0.018833, 0.655209],
- [0.517933, 0.021563, 0.654109],
- [0.523633, 0.024532, 0.652901],
- [0.529306, 0.027747, 0.651586],
- [0.534952, 0.031217, 0.650165],
- [0.540570, 0.034950, 0.648640],
- [0.546157, 0.038954, 0.647010],
- [0.551715, 0.043136, 0.645277],
- [0.557243, 0.047331, 0.643443],
- [0.562738, 0.051545, 0.641509],
- [0.568201, 0.055778, 0.639477],
- [0.573632, 0.060028, 0.637349],
- [0.579029, 0.064296, 0.635126],
- [0.584391, 0.068579, 0.632812],
- [0.589719, 0.072878, 0.630408],
- [0.595011, 0.077190, 0.627917],
- [0.600266, 0.081516, 0.625342],
- [0.605485, 0.085854, 0.622686],
- [0.610667, 0.090204, 0.619951],
- [0.615812, 0.094564, 0.617140],
- [0.620919, 0.098934, 0.614257],
- [0.625987, 0.103312, 0.611305],
- [0.631017, 0.107699, 0.608287],
- [0.636008, 0.112092, 0.605205],
- [0.640959, 0.116492, 0.602065],
- [0.645872, 0.120898, 0.598867],
- [0.650746, 0.125309, 0.595617],
- [0.655580, 0.129725, 0.592317],
- [0.660374, 0.134144, 0.588971],
- [0.665129, 0.138566, 0.585582],
- [0.669845, 0.142992, 0.582154],
- [0.674522, 0.147419, 0.578688],
- [0.679160, 0.151848, 0.575189],
- [0.683758, 0.156278, 0.571660],
- [0.688318, 0.160709, 0.568103],
- [0.692840, 0.165141, 0.564522],
- [0.697324, 0.169573, 0.560919],
- [0.701769, 0.174005, 0.557296],
- [0.706178, 0.178437, 0.553657],
- [0.710549, 0.182868, 0.550004],
- [0.714883, 0.187299, 0.546338],
- [0.719181, 0.191729, 0.542663],
- [0.723444, 0.196158, 0.538981],
- [0.727670, 0.200586, 0.535293],
- [0.731862, 0.205013, 0.531601],
- [0.736019, 0.209439, 0.527908],
- [0.740143, 0.213864, 0.524216],
- [0.744232, 0.218288, 0.520524],
- [0.748289, 0.222711, 0.516834],
- [0.752312, 0.227133, 0.513149],
- [0.756304, 0.231555, 0.509468],
- [0.760264, 0.235976, 0.505794],
- [0.764193, 0.240396, 0.502126],
- [0.768090, 0.244817, 0.498465],
- [0.771958, 0.249237, 0.494813],
- [0.775796, 0.253658, 0.491171],
- [0.779604, 0.258078, 0.487539],
- [0.783383, 0.262500, 0.483918],
- [0.787133, 0.266922, 0.480307],
- [0.790855, 0.271345, 0.476706],
- [0.794549, 0.275770, 0.473117],
- [0.798216, 0.280197, 0.469538],
- [0.801855, 0.284626, 0.465971],
- [0.805467, 0.289057, 0.462415],
- [0.809052, 0.293491, 0.458870],
- [0.812612, 0.297928, 0.455338],
- [0.816144, 0.302368, 0.451816],
- [0.819651, 0.306812, 0.448306],
- [0.823132, 0.311261, 0.444806],
- [0.826588, 0.315714, 0.441316],
- [0.830018, 0.320172, 0.437836],
- [0.833422, 0.324635, 0.434366],
- [0.836801, 0.329105, 0.430905],
- [0.840155, 0.333580, 0.427455],
- [0.843484, 0.338062, 0.424013],
- [0.846788, 0.342551, 0.420579],
- [0.850066, 0.347048, 0.417153],
- [0.853319, 0.351553, 0.413734],
- [0.856547, 0.356066, 0.410322],
- [0.859750, 0.360588, 0.406917],
- [0.862927, 0.365119, 0.403519],
- [0.866078, 0.369660, 0.400126],
- [0.869203, 0.374212, 0.396738],
- [0.872303, 0.378774, 0.393355],
- [0.875376, 0.383347, 0.389976],
- [0.878423, 0.387932, 0.386600],
- [0.881443, 0.392529, 0.383229],
- [0.884436, 0.397139, 0.379860],
- [0.887402, 0.401762, 0.376494],
- [0.890340, 0.406398, 0.373130],
- [0.893250, 0.411048, 0.369768],
- [0.896131, 0.415712, 0.366407],
- [0.898984, 0.420392, 0.363047],
- [0.901807, 0.425087, 0.359688],
- [0.904601, 0.429797, 0.356329],
- [0.907365, 0.434524, 0.352970],
- [0.910098, 0.439268, 0.349610],
- [0.912800, 0.444029, 0.346251],
- [0.915471, 0.448807, 0.342890],
- [0.918109, 0.453603, 0.339529],
- [0.920714, 0.458417, 0.336166],
- [0.923287, 0.463251, 0.332801],
- [0.925825, 0.468103, 0.329435],
- [0.928329, 0.472975, 0.326067],
- [0.930798, 0.477867, 0.322697],
- [0.933232, 0.482780, 0.319325],
- [0.935630, 0.487712, 0.315952],
- [0.937990, 0.492667, 0.312575],
- [0.940313, 0.497642, 0.309197],
- [0.942598, 0.502639, 0.305816],
- [0.944844, 0.507658, 0.302433],
- [0.947051, 0.512699, 0.299049],
- [0.949217, 0.517763, 0.295662],
- [0.951344, 0.522850, 0.292275],
- [0.953428, 0.527960, 0.288883],
- [0.955470, 0.533093, 0.285490],
- [0.957469, 0.538250, 0.282096],
- [0.959424, 0.543431, 0.278701],
- [0.961336, 0.548636, 0.275305],
- [0.963203, 0.553865, 0.271909],
- [0.965024, 0.559118, 0.268513],
- [0.966798, 0.564396, 0.265118],
- [0.968526, 0.569700, 0.261721],
- [0.970205, 0.575028, 0.258325],
- [0.971835, 0.580382, 0.254931],
- [0.973416, 0.585761, 0.251540],
- [0.974947, 0.591165, 0.248151],
- [0.976428, 0.596595, 0.244767],
- [0.977856, 0.602051, 0.241387],
- [0.979233, 0.607532, 0.238013],
- [0.980556, 0.613039, 0.234646],
- [0.981826, 0.618572, 0.231287],
- [0.983041, 0.624131, 0.227937],
- [0.984199, 0.629718, 0.224595],
- [0.985301, 0.635330, 0.221265],
- [0.986345, 0.640969, 0.217948],
- [0.987332, 0.646633, 0.214648],
- [0.988260, 0.652325, 0.211364],
- [0.989128, 0.658043, 0.208100],
- [0.989935, 0.663787, 0.204859],
- [0.990681, 0.669558, 0.201642],
- [0.991365, 0.675355, 0.198453],
- [0.991985, 0.681179, 0.195295],
- [0.992541, 0.687030, 0.192170],
- [0.993032, 0.692907, 0.189084],
- [0.993456, 0.698810, 0.186041],
- [0.993814, 0.704741, 0.183043],
- [0.994103, 0.710698, 0.180097],
- [0.994324, 0.716681, 0.177208],
- [0.994474, 0.722691, 0.174381],
- [0.994553, 0.728728, 0.171622],
- [0.994561, 0.734791, 0.168938],
- [0.994495, 0.740880, 0.166335],
- [0.994355, 0.746995, 0.163821],
- [0.994141, 0.753137, 0.161404],
- [0.993851, 0.759304, 0.159092],
- [0.993482, 0.765499, 0.156891],
- [0.993033, 0.771720, 0.154808],
- [0.992505, 0.777967, 0.152855],
- [0.991897, 0.784239, 0.151042],
- [0.991209, 0.790537, 0.149377],
- [0.990439, 0.796859, 0.147870],
- [0.989587, 0.803205, 0.146529],
- [0.988648, 0.809579, 0.145357],
- [0.987621, 0.815978, 0.144363],
- [0.986509, 0.822401, 0.143557],
- [0.985314, 0.828846, 0.142945],
- [0.984031, 0.835315, 0.142528],
- [0.982653, 0.841812, 0.142303],
- [0.981190, 0.848329, 0.142279],
- [0.979644, 0.854866, 0.142453],
- [0.977995, 0.861432, 0.142808],
- [0.976265, 0.868016, 0.143351],
- [0.974443, 0.874622, 0.144061],
- [0.972530, 0.881250, 0.144923],
- [0.970533, 0.887896, 0.145919],
- [0.968443, 0.894564, 0.147014],
- [0.966271, 0.901249, 0.148180],
- [0.964021, 0.907950, 0.149370],
- [0.961681, 0.914672, 0.150520],
- [0.959276, 0.921407, 0.151566],
- [0.956808, 0.928152, 0.152409],
- [0.954287, 0.934908, 0.152921],
- [0.951726, 0.941671, 0.152925],
- [0.949151, 0.948435, 0.152178],
- [0.946602, 0.955190, 0.150328],
- [0.944152, 0.961916, 0.146861],
- [0.941896, 0.968590, 0.140956],
- [0.940015, 0.975158, 0.131326]]
-
-_viridis_data = [[0.267004, 0.004874, 0.329415],
- [0.268510, 0.009605, 0.335427],
- [0.269944, 0.014625, 0.341379],
- [0.271305, 0.019942, 0.347269],
- [0.272594, 0.025563, 0.353093],
- [0.273809, 0.031497, 0.358853],
- [0.274952, 0.037752, 0.364543],
- [0.276022, 0.044167, 0.370164],
- [0.277018, 0.050344, 0.375715],
- [0.277941, 0.056324, 0.381191],
- [0.278791, 0.062145, 0.386592],
- [0.279566, 0.067836, 0.391917],
- [0.280267, 0.073417, 0.397163],
- [0.280894, 0.078907, 0.402329],
- [0.281446, 0.084320, 0.407414],
- [0.281924, 0.089666, 0.412415],
- [0.282327, 0.094955, 0.417331],
- [0.282656, 0.100196, 0.422160],
- [0.282910, 0.105393, 0.426902],
- [0.283091, 0.110553, 0.431554],
- [0.283197, 0.115680, 0.436115],
- [0.283229, 0.120777, 0.440584],
- [0.283187, 0.125848, 0.444960],
- [0.283072, 0.130895, 0.449241],
- [0.282884, 0.135920, 0.453427],
- [0.282623, 0.140926, 0.457517],
- [0.282290, 0.145912, 0.461510],
- [0.281887, 0.150881, 0.465405],
- [0.281412, 0.155834, 0.469201],
- [0.280868, 0.160771, 0.472899],
- [0.280255, 0.165693, 0.476498],
- [0.279574, 0.170599, 0.479997],
- [0.278826, 0.175490, 0.483397],
- [0.278012, 0.180367, 0.486697],
- [0.277134, 0.185228, 0.489898],
- [0.276194, 0.190074, 0.493001],
- [0.275191, 0.194905, 0.496005],
- [0.274128, 0.199721, 0.498911],
- [0.273006, 0.204520, 0.501721],
- [0.271828, 0.209303, 0.504434],
- [0.270595, 0.214069, 0.507052],
- [0.269308, 0.218818, 0.509577],
- [0.267968, 0.223549, 0.512008],
- [0.266580, 0.228262, 0.514349],
- [0.265145, 0.232956, 0.516599],
- [0.263663, 0.237631, 0.518762],
- [0.262138, 0.242286, 0.520837],
- [0.260571, 0.246922, 0.522828],
- [0.258965, 0.251537, 0.524736],
- [0.257322, 0.256130, 0.526563],
- [0.255645, 0.260703, 0.528312],
- [0.253935, 0.265254, 0.529983],
- [0.252194, 0.269783, 0.531579],
- [0.250425, 0.274290, 0.533103],
- [0.248629, 0.278775, 0.534556],
- [0.246811, 0.283237, 0.535941],
- [0.244972, 0.287675, 0.537260],
- [0.243113, 0.292092, 0.538516],
- [0.241237, 0.296485, 0.539709],
- [0.239346, 0.300855, 0.540844],
- [0.237441, 0.305202, 0.541921],
- [0.235526, 0.309527, 0.542944],
- [0.233603, 0.313828, 0.543914],
- [0.231674, 0.318106, 0.544834],
- [0.229739, 0.322361, 0.545706],
- [0.227802, 0.326594, 0.546532],
- [0.225863, 0.330805, 0.547314],
- [0.223925, 0.334994, 0.548053],
- [0.221989, 0.339161, 0.548752],
- [0.220057, 0.343307, 0.549413],
- [0.218130, 0.347432, 0.550038],
- [0.216210, 0.351535, 0.550627],
- [0.214298, 0.355619, 0.551184],
- [0.212395, 0.359683, 0.551710],
- [0.210503, 0.363727, 0.552206],
- [0.208623, 0.367752, 0.552675],
- [0.206756, 0.371758, 0.553117],
- [0.204903, 0.375746, 0.553533],
- [0.203063, 0.379716, 0.553925],
- [0.201239, 0.383670, 0.554294],
- [0.199430, 0.387607, 0.554642],
- [0.197636, 0.391528, 0.554969],
- [0.195860, 0.395433, 0.555276],
- [0.194100, 0.399323, 0.555565],
- [0.192357, 0.403199, 0.555836],
- [0.190631, 0.407061, 0.556089],
- [0.188923, 0.410910, 0.556326],
- [0.187231, 0.414746, 0.556547],
- [0.185556, 0.418570, 0.556753],
- [0.183898, 0.422383, 0.556944],
- [0.182256, 0.426184, 0.557120],
- [0.180629, 0.429975, 0.557282],
- [0.179019, 0.433756, 0.557430],
- [0.177423, 0.437527, 0.557565],
- [0.175841, 0.441290, 0.557685],
- [0.174274, 0.445044, 0.557792],
- [0.172719, 0.448791, 0.557885],
- [0.171176, 0.452530, 0.557965],
- [0.169646, 0.456262, 0.558030],
- [0.168126, 0.459988, 0.558082],
- [0.166617, 0.463708, 0.558119],
- [0.165117, 0.467423, 0.558141],
- [0.163625, 0.471133, 0.558148],
- [0.162142, 0.474838, 0.558140],
- [0.160665, 0.478540, 0.558115],
- [0.159194, 0.482237, 0.558073],
- [0.157729, 0.485932, 0.558013],
- [0.156270, 0.489624, 0.557936],
- [0.154815, 0.493313, 0.557840],
- [0.153364, 0.497000, 0.557724],
- [0.151918, 0.500685, 0.557587],
- [0.150476, 0.504369, 0.557430],
- [0.149039, 0.508051, 0.557250],
- [0.147607, 0.511733, 0.557049],
- [0.146180, 0.515413, 0.556823],
- [0.144759, 0.519093, 0.556572],
- [0.143343, 0.522773, 0.556295],
- [0.141935, 0.526453, 0.555991],
- [0.140536, 0.530132, 0.555659],
- [0.139147, 0.533812, 0.555298],
- [0.137770, 0.537492, 0.554906],
- [0.136408, 0.541173, 0.554483],
- [0.135066, 0.544853, 0.554029],
- [0.133743, 0.548535, 0.553541],
- [0.132444, 0.552216, 0.553018],
- [0.131172, 0.555899, 0.552459],
- [0.129933, 0.559582, 0.551864],
- [0.128729, 0.563265, 0.551229],
- [0.127568, 0.566949, 0.550556],
- [0.126453, 0.570633, 0.549841],
- [0.125394, 0.574318, 0.549086],
- [0.124395, 0.578002, 0.548287],
- [0.123463, 0.581687, 0.547445],
- [0.122606, 0.585371, 0.546557],
- [0.121831, 0.589055, 0.545623],
- [0.121148, 0.592739, 0.544641],
- [0.120565, 0.596422, 0.543611],
- [0.120092, 0.600104, 0.542530],
- [0.119738, 0.603785, 0.541400],
- [0.119512, 0.607464, 0.540218],
- [0.119423, 0.611141, 0.538982],
- [0.119483, 0.614817, 0.537692],
- [0.119699, 0.618490, 0.536347],
- [0.120081, 0.622161, 0.534946],
- [0.120638, 0.625828, 0.533488],
- [0.121380, 0.629492, 0.531973],
- [0.122312, 0.633153, 0.530398],
- [0.123444, 0.636809, 0.528763],
- [0.124780, 0.640461, 0.527068],
- [0.126326, 0.644107, 0.525311],
- [0.128087, 0.647749, 0.523491],
- [0.130067, 0.651384, 0.521608],
- [0.132268, 0.655014, 0.519661],
- [0.134692, 0.658636, 0.517649],
- [0.137339, 0.662252, 0.515571],
- [0.140210, 0.665859, 0.513427],
- [0.143303, 0.669459, 0.511215],
- [0.146616, 0.673050, 0.508936],
- [0.150148, 0.676631, 0.506589],
- [0.153894, 0.680203, 0.504172],
- [0.157851, 0.683765, 0.501686],
- [0.162016, 0.687316, 0.499129],
- [0.166383, 0.690856, 0.496502],
- [0.170948, 0.694384, 0.493803],
- [0.175707, 0.697900, 0.491033],
- [0.180653, 0.701402, 0.488189],
- [0.185783, 0.704891, 0.485273],
- [0.191090, 0.708366, 0.482284],
- [0.196571, 0.711827, 0.479221],
- [0.202219, 0.715272, 0.476084],
- [0.208030, 0.718701, 0.472873],
- [0.214000, 0.722114, 0.469588],
- [0.220124, 0.725509, 0.466226],
- [0.226397, 0.728888, 0.462789],
- [0.232815, 0.732247, 0.459277],
- [0.239374, 0.735588, 0.455688],
- [0.246070, 0.738910, 0.452024],
- [0.252899, 0.742211, 0.448284],
- [0.259857, 0.745492, 0.444467],
- [0.266941, 0.748751, 0.440573],
- [0.274149, 0.751988, 0.436601],
- [0.281477, 0.755203, 0.432552],
- [0.288921, 0.758394, 0.428426],
- [0.296479, 0.761561, 0.424223],
- [0.304148, 0.764704, 0.419943],
- [0.311925, 0.767822, 0.415586],
- [0.319809, 0.770914, 0.411152],
- [0.327796, 0.773980, 0.406640],
- [0.335885, 0.777018, 0.402049],
- [0.344074, 0.780029, 0.397381],
- [0.352360, 0.783011, 0.392636],
- [0.360741, 0.785964, 0.387814],
- [0.369214, 0.788888, 0.382914],
- [0.377779, 0.791781, 0.377939],
- [0.386433, 0.794644, 0.372886],
- [0.395174, 0.797475, 0.367757],
- [0.404001, 0.800275, 0.362552],
- [0.412913, 0.803041, 0.357269],
- [0.421908, 0.805774, 0.351910],
- [0.430983, 0.808473, 0.346476],
- [0.440137, 0.811138, 0.340967],
- [0.449368, 0.813768, 0.335384],
- [0.458674, 0.816363, 0.329727],
- [0.468053, 0.818921, 0.323998],
- [0.477504, 0.821444, 0.318195],
- [0.487026, 0.823929, 0.312321],
- [0.496615, 0.826376, 0.306377],
- [0.506271, 0.828786, 0.300362],
- [0.515992, 0.831158, 0.294279],
- [0.525776, 0.833491, 0.288127],
- [0.535621, 0.835785, 0.281908],
- [0.545524, 0.838039, 0.275626],
- [0.555484, 0.840254, 0.269281],
- [0.565498, 0.842430, 0.262877],
- [0.575563, 0.844566, 0.256415],
- [0.585678, 0.846661, 0.249897],
- [0.595839, 0.848717, 0.243329],
- [0.606045, 0.850733, 0.236712],
- [0.616293, 0.852709, 0.230052],
- [0.626579, 0.854645, 0.223353],
- [0.636902, 0.856542, 0.216620],
- [0.647257, 0.858400, 0.209861],
- [0.657642, 0.860219, 0.203082],
- [0.668054, 0.861999, 0.196293],
- [0.678489, 0.863742, 0.189503],
- [0.688944, 0.865448, 0.182725],
- [0.699415, 0.867117, 0.175971],
- [0.709898, 0.868751, 0.169257],
- [0.720391, 0.870350, 0.162603],
- [0.730889, 0.871916, 0.156029],
- [0.741388, 0.873449, 0.149561],
- [0.751884, 0.874951, 0.143228],
- [0.762373, 0.876424, 0.137064],
- [0.772852, 0.877868, 0.131109],
- [0.783315, 0.879285, 0.125405],
- [0.793760, 0.880678, 0.120005],
- [0.804182, 0.882046, 0.114965],
- [0.814576, 0.883393, 0.110347],
- [0.824940, 0.884720, 0.106217],
- [0.835270, 0.886029, 0.102646],
- [0.845561, 0.887322, 0.099702],
- [0.855810, 0.888601, 0.097452],
- [0.866013, 0.889868, 0.095953],
- [0.876168, 0.891125, 0.095250],
- [0.886271, 0.892374, 0.095374],
- [0.896320, 0.893616, 0.096335],
- [0.906311, 0.894855, 0.098125],
- [0.916242, 0.896091, 0.100717],
- [0.926106, 0.897330, 0.104071],
- [0.935904, 0.898570, 0.108131],
- [0.945636, 0.899815, 0.112838],
- [0.955300, 0.901065, 0.118128],
- [0.964894, 0.902323, 0.123941],
- [0.974417, 0.903590, 0.130215],
- [0.983868, 0.904867, 0.136897],
- [0.993248, 0.906157, 0.143936]]
-
-
-cmaps = {}
-for (name, data) in (('magma', _magma_data),
- ('inferno', _inferno_data),
- ('plasma', _plasma_data),
- ('viridis', _viridis_data)):
-
- cmaps[name] = ListedColormap(data, name=name)
-
-magma = cmaps['magma']
-inferno = cmaps['inferno']
-plasma = cmaps['plasma']
-viridis = cmaps['viridis']
diff --git a/silx/gui/plot/MaskToolsWidget.py b/silx/gui/plot/MaskToolsWidget.py
index 6407d44..09c5ca5 100644
--- a/silx/gui/plot/MaskToolsWidget.py
+++ b/silx/gui/plot/MaskToolsWidget.py
@@ -35,13 +35,14 @@ from __future__ import division
__authors__ = ["T. Vincent", "P. Knobel"]
__license__ = "MIT"
-__date__ = "20/04/2017"
+__date__ = "20/06/2017"
import os
import sys
import numpy
import logging
+import collections
from silx.image import shapes
@@ -211,17 +212,13 @@ class MaskToolsWidget(BaseMaskToolsWidget):
_maxLevelNumber = 255
def __init__(self, parent=None, plot=None):
+ super(MaskToolsWidget, self).__init__(parent, plot,
+ mask=ImageMask())
self._origin = (0., 0.) # Mask origin in plot
self._scale = (1., 1.) # Mask scale in plot
self._z = 1 # Mask layer in plot
self._data = numpy.zeros((0, 0), dtype=numpy.uint8) # Store image
- self._mask = ImageMask()
-
- super(MaskToolsWidget, self).__init__(parent, plot)
-
- self._initWidgets()
-
def setSelectionMask(self, mask, copy=True):
"""Set the mask to a new array.
@@ -239,6 +236,13 @@ class MaskToolsWidget(BaseMaskToolsWidget):
_logger.error('Not an image, shape: %d', len(mask.shape))
return None
+ # ensure all mask attributes are synchronized with the active image
+ # and connect listener
+ activeImage = self.plot.getActiveImage()
+ if activeImage is not None and activeImage.getLegend() != self._maskName:
+ self._activeImageChanged()
+ self.plot.sigActiveImageChanged.connect(self._activeImageChanged)
+
if self._data.shape[0:2] == (0, 0) or mask.shape == self._data.shape[0:2]:
self._mask.setMask(mask, copy=copy)
self._mask.commit()
@@ -262,12 +266,22 @@ class MaskToolsWidget(BaseMaskToolsWidget):
"""Update mask image in plot"""
mask = self.getSelectionMask(copy=False)
if len(mask):
- self.plot.addImage(mask, legend=self._maskName,
- colormap=self._colormap,
- origin=self._origin,
- scale=self._scale,
- z=self._z,
- replace=False, resetzoom=False)
+ # get the mask from the plot
+ maskItem = self.plot.getImage(self._maskName)
+ mustBeAdded = maskItem is None
+ if mustBeAdded:
+ maskItem = items.MaskImageData()
+ maskItem._setLegend(self._maskName)
+ # update the items
+ maskItem.setData(mask, copy=False)
+ maskItem.setColormap(self._colormap)
+ maskItem.setOrigin(self._origin)
+ maskItem.setScale(self._scale)
+ maskItem.setZValue(self._z)
+
+ if mustBeAdded:
+ self.plot._add(maskItem)
+
elif self.plot.getImage(self._maskName):
self.plot.remove(self._maskName, kind='image')
@@ -281,7 +295,11 @@ class MaskToolsWidget(BaseMaskToolsWidget):
self.plot.sigActiveImageChanged.connect(self._activeImageChanged)
def hideEvent(self, event):
- self.plot.sigActiveImageChanged.disconnect(self._activeImageChanged)
+ try:
+ self.plot.sigActiveImageChanged.disconnect(
+ self._activeImageChanged)
+ except (RuntimeError, TypeError):
+ pass
if not self.browseAction.isChecked():
self.browseAction.trigger() # Disable drawing tool
@@ -337,8 +355,9 @@ class MaskToolsWidget(BaseMaskToolsWidget):
def _activeImageChanged(self, *args):
"""Update widget and mask according to active image changes"""
activeImage = self.plot.getActiveImage()
- if activeImage is None or activeImage.getLegend() == self._maskName:
- # No active image or active image is the mask...
+ if (activeImage is None or activeImage.getLegend() == self._maskName or
+ activeImage.getData(copy=False).size == 0):
+ # No active image or active image is the mask or image has no data...
self.setEnabled(False)
self._data = numpy.zeros((0, 0), dtype=numpy.uint8)
@@ -390,6 +409,14 @@ class MaskToolsWidget(BaseMaskToolsWidget):
_logger.error("Can't load filename '%s'", filename)
_logger.debug("Backtrace", exc_info=True)
raise RuntimeError('File "%s" is not a numpy file.', filename)
+ elif extension in ["tif", "tiff"]:
+ try:
+ image = TiffIO(filename, mode="r")
+ mask = image.getImage(0)
+ except Exception as e:
+ _logger.error("Can't load filename %s", filename)
+ _logger.debug("Backtrace", exc_info=True)
+ raise e
elif extension == "edf":
try:
mask = EdfFile(filename, access='r').GetData(0)
@@ -423,14 +450,21 @@ class MaskToolsWidget(BaseMaskToolsWidget):
dialog = qt.QFileDialog(self)
dialog.setWindowTitle("Load Mask")
dialog.setModal(1)
- filters = [
- 'EDF (*.edf)',
- 'TIFF (*.tif)',
- 'NumPy binary file (*.npy)',
- # Fit2D mask is displayed anyway fabio is here or not
- # to show to the user that the option exists
- 'Fit2D mask (*.msk)',
- ]
+
+ extensions = collections.OrderedDict()
+ extensions["EDF files"] = "*.edf"
+ extensions["TIFF files"] = "*.tif *.tiff"
+ extensions["NumPy binary files"] = "*.npy"
+ # Fit2D mask is displayed anyway fabio is here or not
+ # to show to the user that the option exists
+ extensions["Fit2D mask files"] = "*.msk"
+
+ filters = []
+ filters.append("All supported files (%s)" % " ".join(extensions.values()))
+ for name, extension in extensions.items():
+ filters.append("%s (%s)" % (name, extension))
+ filters.append("All files (*)")
+
dialog.setNameFilters(filters)
dialog.setFileMode(qt.QFileDialog.ExistingFile)
dialog.setDirectory(self.maskFileDir)
@@ -610,6 +644,5 @@ class MaskToolsDockWidget(BaseMaskToolsDockWidget):
:paran str name: The title of this widget
"""
def __init__(self, parent=None, plot=None, name='Mask'):
- super(MaskToolsDockWidget, self).__init__(parent, name)
- self.setWidget(MaskToolsWidget(plot=plot))
- self.widget().sigMaskChanged.connect(self._emitSigMaskChanged)
+ widget = MaskToolsWidget(plot=plot)
+ super(MaskToolsDockWidget, self).__init__(parent, name, widget)
diff --git a/silx/gui/plot/Plot.py b/silx/gui/plot/Plot.py
deleted file mode 100644
index fe0a7b8..0000000
--- a/silx/gui/plot/Plot.py
+++ /dev/null
@@ -1,2925 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2004-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.
-# ###########################################################################*/
-"""Plot API for 1D and 2D data.
-
-The :class:`Plot` implements the plot API initially provided in PyMca.
-
-
-Colormap
---------
-
-The :class:`Plot` uses a dictionary to describe a colormap.
-This dictionary has the following keys:
-
-- 'name': str, name of the colormap. Available colormap are returned by
- :meth:`Plot.getSupportedColormaps`.
- At least 'gray', 'reversed gray', 'temperature',
- 'red', 'green', 'blue' are supported.
-- 'normalization': Either 'linear' or 'log'
-- 'autoscale': bool, True to get bounds from the min and max of the
- data, False to use [vmin, vmax]
-- 'vmin': float, min value, ignored if autoscale is True
-- 'vmax': float, max value, ignored if autoscale is True
-- 'colors': optional, custom colormap.
- Nx3 or Nx4 numpy array of RGB(A) colors,
- either uint8 or float in [0, 1].
- If 'name' is None, then this array is used as the colormap.
-
-
-Plot Events
------------
-
-The Plot sends some event to the registered callback
-(See :meth:`Plot.setCallback`).
-Those events are sent as a dictionary with a key 'event' describing the kind
-of event.
-
-Drawing events
-..............
-
-'drawingProgress' and 'drawingFinished' events are sent during drawing
-interaction (See :meth:`Plot.setInteractiveMode`).
-
-- 'event': 'drawingProgress' or 'drawingFinished'
-- 'parameters': dict of parameters used by the drawing mode.
- It has the following keys: 'shape', 'label', 'color'.
- See :meth:`Plot.setInteractiveMode`.
-- 'points': Points (x, y) in data coordinates of the drawn shape.
- For 'hline' and 'vline', it is the 2 points defining the line.
- For 'line' and 'rectangle', it is the coordinates of the start
- drawing point and the latest drawing point.
- For 'polygon', it is the coordinates of all points of the shape.
-- 'type': The type of drawing in 'line', 'hline', 'polygon', 'rectangle',
- 'vline'.
-- 'xdata' and 'ydata': X coords and Y coords of shape points in data
- coordinates (as in 'points').
-
-When the type is 'rectangle', the following additional keys are provided:
-
-- 'x' and 'y': The origin of the rectangle in data coordinates
-- 'widht' and 'height': The size of the rectangle in data coordinates
-
-
-Mouse events
-............
-
-'mouseMoved', 'mouseClicked' and 'mouseDoubleClicked' events are sent for
-mouse events.
-
-They provide the following keys:
-
-- 'event': 'mouseMoved', 'mouseClicked' or 'mouseDoubleClicked'
-- 'button': the mouse button that was pressed in 'left', 'middle', 'right'
-- 'x' and 'y': The mouse position in data coordinates
-- 'xpixel' and 'ypixel': The mouse position in pixels
-
-
-Marker events
-.............
-
-'hover', 'markerClicked', 'markerMoving' and 'markerMoved' events are
-sent during interaction with markers.
-
-'hover' is sent when the mouse cursor is over a marker.
-'markerClicker' is sent when the user click on a selectable marker.
-'markerMoving' and 'markerMoved' are sent when a draggable marker is moved.
-
-They provide the following keys:
-
-- 'event': 'hover', 'markerClicked', 'markerMoving' or 'markerMoved'
-- 'button': the mouse button that is pressed in 'left', 'middle', 'right'
-- 'draggable': True if the marker is draggable, False otherwise
-- 'label': The legend associated with the clicked image or curve
-- 'selectable': True if the marker is selectable, False otherwise
-- 'type': 'marker'
-- 'x' and 'y': The mouse position in data coordinates
-- 'xdata' and 'ydata': The marker position in data coordinates
-
-'markerClicked' and 'markerMoving' events have a 'xpixel' and a 'ypixel'
-additional keys, that provide the mouse position in pixels.
-
-
-Image and curve events
-......................
-
-'curveClicked' and 'imageClicked' events are sent when a selectable curve
-or image is clicked.
-
-Both share the following keys:
-
-- 'event': 'curveClicked' or 'imageClicked'
-- 'button': the mouse button that was pressed in 'left', 'middle', 'right'
-- 'label': The legend associated with the clicked image or curve
-- 'type': The type of item in 'curve', 'image'
-- 'x' and 'y': The clicked position in data coordinates
-- 'xpixel' and 'ypixel': The clicked position in pixels
-
-'curveClicked' events have a 'xdata' and a 'ydata' additional keys, that
-provide the coordinates of the picked points of the curve.
-There can be more than one point of the curve being picked, and if a line of
-the curve is picked, only the first point of the line is included in the list.
-
-'imageClicked' have a 'col' and a 'row' additional keys, that provide
-the column and row index in the image array that was clicked.
-
-
-Limits changed events
-.....................
-
-'limitsChanged' events are sent when the limits of the plot are changed.
-This can results from user interaction or API calls.
-
-It provides the following keys:
-
-- 'event': 'limitsChanged'
-- 'source': id of the widget that emitted this event.
-- 'xdata': Range of X in graph coordinates: (xMin, xMax).
-- 'ydata': Range of Y in graph coordinates: (yMin, yMax).
-- 'y2data': Range of right axis in graph coordinates (y2Min, y2Max) or None.
-
-Plot state change events
-........................
-
-The following events are emitted when the plot is modified.
-They provide the new state:
-
-- 'setGraphCursor' event with a 'state' key (bool)
-- 'setGraphGrid' event with a 'which' key (str), see :meth:`setGraphGrid`
-- 'setKeepDataAspectRatio' event with a 'state' key (bool)
-- 'setXAxisAutoScale' event with a 'state' key (bool)
-- 'setXAxisLogarithmic' event with a 'state' key (bool)
-- 'setYAxisAutoScale' event with a 'state' key (bool)
-- 'setYAxisInverted' event with a 'state' key (bool)
-- 'setYAxisLogarithmic' event with a 'state' key (bool)
-
-A 'contentChanged' event is triggered when the content of the plot is updated.
-It provides the following keys:
-
-- 'action': The change of the plot: 'add' or 'remove'
-- 'kind': The kind of primitive changed: 'curve', 'image', 'item' or 'marker'
-- 'legend': The legend of the primitive changed.
-
-'activeCurveChanged' and 'activeImageChanged' events with the following keys:
-
-- 'legend': Name (str) of the current active item or None if no active item.
-- 'previous': Name (str) of the previous active item or None if no item was
- active. It is the same as 'legend' if 'updated' == True
-- 'updated': (bool) True if active item name did not changed,
- but active item data or style was updated.
-
-'interactiveModeChanged' event with a 'source' key identifying the object
-setting the interactive mode.
-"""
-
-from __future__ import division
-
-
-__authors__ = ["V.A. Sole", "T. Vincent"]
-__license__ = "MIT"
-__date__ = "16/02/2017"
-
-
-from collections import OrderedDict, namedtuple
-import itertools
-import logging
-
-import numpy
-
-# Import matplotlib backend here to init matplotlib our way
-from .backends.BackendMatplotlib import BackendMatplotlibQt
-
-try:
- from matplotlib import cm as matplotlib_cm
-except ImportError:
- matplotlib_cm = None
-
-from . import Colors
-from . import PlotInteraction
-from . import PlotEvents
-from . import _utils
-
-from . import items
-
-
-_logger = logging.getLogger(__name__)
-
-
-_COLORDICT = Colors.COLORDICT
-_COLORLIST = [_COLORDICT['black'],
- _COLORDICT['blue'],
- _COLORDICT['red'],
- _COLORDICT['green'],
- _COLORDICT['pink'],
- _COLORDICT['yellow'],
- _COLORDICT['brown'],
- _COLORDICT['cyan'],
- _COLORDICT['magenta'],
- _COLORDICT['orange'],
- _COLORDICT['violet'],
- # _COLORDICT['bluegreen'],
- _COLORDICT['grey'],
- _COLORDICT['darkBlue'],
- _COLORDICT['darkRed'],
- _COLORDICT['darkGreen'],
- _COLORDICT['darkCyan'],
- _COLORDICT['darkMagenta'],
- _COLORDICT['darkYellow'],
- _COLORDICT['darkBrown']]
-
-
-"""
-Object returned when requesting the data range.
-"""
-_PlotDataRange = namedtuple('PlotDataRange',
- ['x', 'y', 'yright'])
-
-
-class Plot(object):
- """This class implements the plot API initially provided in PyMca.
-
- Supported backends:
-
- - 'matplotlib' and 'mpl': Matplotlib with Qt.
- - 'opengl' and 'gl': OpenGL backend (requires PyOpenGL and OpenGL >= 2.1)
- - 'none': No backend, to run headless for testing purpose.
-
- :param parent: The parent widget of the plot (Default: None)
- :param backend: The backend to use. A str in:
- 'matplotlib', 'mpl', 'opengl', 'gl', 'none'
- or a :class:`BackendBase.BackendBase` class
- """
-
- DEFAULT_BACKEND = 'matplotlib'
- """Class attribute setting the default backend for all instances."""
-
- colorList = _COLORLIST
- colorDict = _COLORDICT
-
- def __init__(self, parent=None, backend=None):
- self._autoreplot = False
- self._dirty = False
- self._cursorInPlot = False
-
- if backend is None:
- backend = self.DEFAULT_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))
-
- super(Plot, self).__init__()
-
- self.setCallback() # set _callback
-
- # Items handling
- self._content = OrderedDict()
- self._contentToUpdate = set()
-
- self._dataRange = None
-
- # line types
- self._styleList = ['-', '--', '-.', ':']
- self._colorIndex = 0
- self._styleIndex = 0
-
- self._activeCurveHandling = True
- self._activeCurveColor = "#000000"
- self._activeLegend = {'curve': None, 'image': None,
- 'scatter': None}
-
- # default properties
- self._cursorConfiguration = None
-
- self._logY = False
- self._logX = False
- self._xAutoScale = True
- self._yAutoScale = True
- self._grid = None
-
- # Store default labels provided to setGraph[X|Y]Label
- self._defaultLabels = {'x': '', 'y': '', 'yright': ''}
- # Store currently displayed labels
- # Current label can differ from input one with active curve handling
- self._currentLabels = {'x': '', 'y': '', 'yright': ''}
-
- self._graphTitle = ''
-
- self.setGraphTitle()
- self.setGraphXLabel()
- self.setGraphYLabel()
- self.setGraphYLabel('', axis='right')
-
- self.setDefaultColormap() # Init default colormap
-
- self.setDefaultPlotPoints(False)
- self.setDefaultPlotLines(True)
-
- self._eventHandler = PlotInteraction.PlotInteraction(self)
- self._eventHandler.setInteractiveMode('zoom', color=(0., 0., 0., 1.))
-
- self._pressedButtons = [] # Currently pressed mouse buttons
-
- self._defaultDataMargins = (0., 0., 0., 0.)
-
- # Only activate autoreplot at the end
- # This avoids errors when loaded in Qt designer
- self._dirty = False
- self._autoreplot = True
-
- def _getDirtyPlot(self):
- """Return the plot dirty flag.
-
- If False, the plot has not changed since last replot.
- If True, the full plot need to be redrawn.
- If 'overlay', only the overlay has changed since last replot.
-
- It can be accessed by backend to check the dirty state.
-
- :return: False, True, 'overlay'
- """
- return self._dirty
-
- def _setDirtyPlot(self, overlayOnly=False):
- """Mark the plot as needing redraw
-
- :param bool overlayOnly: True to redraw only the overlay,
- False to redraw everything
- """
- wasDirty = self._dirty
-
- if not self._dirty and overlayOnly:
- self._dirty = 'overlay'
- else:
- self._dirty = True
-
- if self._autoreplot and not wasDirty:
- self._backend.postRedisplay()
-
- def _invalidateDataRange(self):
- """
- Notifies this Plot instance that the range has changed and will have
- to be recomputed.
- """
- self._dataRange = None
-
- def _updateDataRange(self):
- """
- Recomputes the range of the data displayed on this Plot.
- """
- xMin = yMinLeft = yMinRight = float('nan')
- xMax = yMaxLeft = yMaxRight = float('nan')
-
- for item in self._content.values():
- if item.isVisible():
- bounds = item.getBounds()
- if bounds is not None:
- 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]])
- else:
- 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)
- xRange = lGetRange(xMin, xMax)
- yLeftRange = lGetRange(yMinLeft, yMaxLeft)
- yRightRange = lGetRange(yMinRight, yMaxRight)
-
- self._dataRange = _PlotDataRange(x=xRange,
- y=yLeftRange,
- yright=yRightRange)
-
- def getDataRange(self):
- """
- Returns this Plot's data range.
-
- :return: a namedtuple with the following members :
- x, y (left y axis), yright. Each member is a tuple (min, max)
- or None if no data is associated with the axis.
- :rtype: namedtuple
- """
- if self._dataRange is None:
- self._updateDataRange()
- return self._dataRange
-
- # Content management
-
- @staticmethod
- def _itemKey(item):
- """Build the key of given :class:`Item` in the plot
-
- :param Item item: The item to make the key from
- :return: (legend, kind)
- :rtype: (str, str)
- """
- if isinstance(item, items.Curve):
- kind = 'curve'
- elif isinstance(item, items.ImageBase):
- kind = 'image'
- elif isinstance(item, items.Scatter):
- kind = 'scatter'
- elif isinstance(item, (items.Marker,
- items.XMarker, items.YMarker)):
- kind = 'marker'
- elif isinstance(item, items.Shape):
- kind = 'item'
- elif isinstance(item, items.Histogram):
- kind = 'histogram'
- else:
- raise ValueError('Unsupported item type %s' % type(item))
-
- return item.getLegend(), kind
-
- def _add(self, item):
- """Add the given :class:`Item` to the plot.
-
- :param Item item: The item to append to the plot content
- """
- key = self._itemKey(item)
- if key in self._content:
- raise RuntimeError('Item already in the plot')
-
- # Add item to plot
- self._content[key] = item
- item._setPlot(self)
- if item.isVisible():
- self._itemRequiresUpdate(item)
- if isinstance(item, (items.Curve, items.ImageBase)):
- self._invalidateDataRange() # TODO handle this automatically
-
- def _remove(self, item):
- """Remove the given :class:`Item` from the plot.
-
- :param Item item: The item to remove from the plot content
- """
- key = self._itemKey(item)
- if key not in self._content:
- raise RuntimeError('Item not in the plot')
-
- # Remove item from plot
- self._content.pop(key)
- self._contentToUpdate.discard(item)
- if item.isVisible():
- self._setDirtyPlot(overlayOnly=item.isOverlay())
- if item.getBounds() is not None:
- self._invalidateDataRange()
- item._removeBackendRenderer(self._backend)
- item._setPlot(None)
-
- def _itemRequiresUpdate(self, item):
- """Called by items in the plot for asynchronous update
-
- :param Item item: The item that required update
- """
- assert item.getPlot() == self
- self._contentToUpdate.add(item)
- self._setDirtyPlot(overlayOnly=item.isOverlay())
-
- # Add
-
- # add * input arguments management:
- # If an arg is set, then use it.
- # Else:
- # If a curve with the same legend exists, then use its arg value
- # Else, use a default value.
- # Store used value.
- # 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,
- 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):
- """Add a 1D curve given by x an y to the graph.
-
- Curves are uniquely identified by their legend.
- To add multiple curves, call :meth:`addCurve` multiple times with
- different legend argument.
- To replace an existing curve, call :meth:`addCurve` with the
- existing curve legend.
- If you want to display the curve values as an histogram see the
- histogram parameter or :meth:`addHistogram`.
-
- When curve parameters are not provided, if a curve with the
- same legend is displayed in the plot, its parameters are used.
-
- :param numpy.ndarray x: The data corresponding to the x coordinates.
- If you attempt to plot an histogram you can set edges values in x.
- In this case len(x) = len(y) + 1
- :param numpy.ndarray y: The data corresponding to the y coordinates
- :param str legend: The legend to be associated to the curve (or None)
- :param info: User-defined information associated to the curve
- :param bool replace: True (the default) to delete already existing
- curves
- :param color: color(s) to be used
- :type color: str ("#RRGGBB") or (npoints, 4) unsigned byte array or
- one of the predefined color names defined in Colors.py
- :param str symbol: Symbol to be drawn at each (x, y) position::
-
- - 'o' circle
- - '.' point
- - ',' pixel
- - '+' cross
- - 'x' x-cross
- - 'd' diamond
- - 's' square
- - None (the default) to use default symbol
-
- :param float linewidth: The width of the curve in pixels (Default: 1).
- :param str linestyle: Type of line::
-
- - ' ' no line
- - '-' solid line
- - '--' dashed line
- - '-.' dash-dot line
- - ':' dotted line
- - None (the default) to use default line style
-
- :param str xlabel: Label to show on the X axis when the curve is active
- or None to keep default axis label.
- :param str ylabel: Label to show on the Y axis when the curve is active
- or None to keep default axis label.
- :param str yaxis: The Y axis this curve is attached to.
- Either 'left' (the default) or 'right'
- :param xerror: Values with the uncertainties on the x values
- :type xerror: A float, or a numpy.ndarray of float32.
- If it is an array, it can either be a 1D array of
- same length as the data or a 2D array with 2 rows
- of same length as the data: row 0 for positive errors,
- row 1 for negative errors.
- :param yerror: Values with the uncertainties on the y values
- :type yerror: A float, or a numpy.ndarray of float32. See xerror.
- :param int z: Layer on which to draw the curve (default: 1)
- This allows to control the overlay.
- :param bool selectable: Indicate if the curve can be selected.
- (Default: True)
- :param bool fill: True to fill the curve, False otherwise (default).
- :param bool resetzoom: True (the default) to reset the zoom.
- :param str histogram: if not None then the curve will be draw as an
- histogram. The step for each values of the curve can be set to the
- left, center or right of the original x curve values.
- If histogram is not None and len(x) == len(y)+1 then x is directly
- take as edges of the histogram.
- Type of histogram::
-
- - None (default)
- - 'left'
- - 'right'
- - 'center'
- :param bool copy: True make a copy of the data (default),
- 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,
- edges=x,
- legend=legend,
- color=color,
- fill=fill,
- align=histogram,
- copy=copy)
- histo = self.getHistogram(histoLegend)
-
- histo.setInfo(info)
- if linewidth is not None:
- histo.setLineWidth(linewidth)
- if linestyle is not None:
- histo.setLineStyle(linestyle)
- if xlabel is not None:
- _logger.warning(
- 'addCurve: Histogram does not support xlabel argument')
- if ylabel is not None:
- _logger.warning(
- 'addCurve: Histogram does not support ylabel argument')
- if yaxis is not None:
- histo.setYAxis(yaxis)
- if z is not None:
- histo.setZValue(z)
- if selectable is not None:
- _logger.warning(
- 'addCurve: Histogram does not support selectable argument')
-
- return
-
- legend = 'Unnamed curve 1.1' if legend is None else str(legend)
-
- # Check if curve was previously active
- wasActive = self.getActiveCurve(just_legend=True) == legend
-
- # Create/Update curve object
- curve = self.getCurve(legend)
- if curve is None:
- # No previous curve, create a default one and add it to the plot
- curve = items.Curve() if histogram is None else items.Histogram()
- curve._setLegend(legend)
- # Set default color, linestyle and symbol
- default_color, default_linestyle = self._getColorAndStyle()
- curve.setColor(default_color)
- curve.setLineStyle(default_linestyle)
- curve.setSymbol(self._defaultPlotPoints)
- self._add(curve)
-
- # Override previous/default values with provided ones
- curve.setInfo(info)
- if color is not None:
- curve.setColor(color)
- if symbol is not None:
- curve.setSymbol(symbol)
- if linewidth is not None:
- curve.setLineWidth(linewidth)
- if linestyle is not None:
- curve.setLineStyle(linestyle)
- if xlabel is not None:
- curve._setXLabel(xlabel)
- if ylabel is not None:
- curve._setYLabel(ylabel)
- if yaxis is not None:
- curve.setYAxis(yaxis)
- if z is not None:
- curve.setZValue(z)
- if selectable is not None:
- curve._setSelectable(selectable)
- if fill is not None:
- curve.setFill(fill)
-
- # Set curve data
- # If errors not provided, reuse previous ones
- # TODO: Issue if size of data change but not that of errors
- if xerror is None:
- xerror = curve.getXErrorData(copy=False)
- if yerror is None:
- yerror = curve.getYErrorData(copy=False)
-
- curve.setData(x, y, xerror, yerror, copy=copy)
-
- if replace: # Then remove all other curves
- for c in self.getAllCurves(withhidden=True):
- if c is not curve:
- self._remove(c)
-
- self.notify(
- 'contentChanged', action='add', kind='curve', legend=legend)
-
- if wasActive:
- self.setActiveCurve(curve.getLegend())
-
- if resetzoom:
- # We ask for a zoom reset in order to handle the plot scaling
- # if the user does not want that, autoscale of the different
- # axes has to be set to off.
- self.resetZoom()
-
- return legend
-
- def addHistogram(self,
- histogram,
- edges,
- legend=None,
- color=None,
- fill=None,
- align='center',
- resetzoom=True,
- copy=True):
- """Add an histogram to the graph.
-
- This is NOT computing the histogram, this method takes as parameter
- already computed histogram values.
-
- Histogram are uniquely identified by their legend.
- To add multiple histograms, call :meth:`addHistogram` multiple times
- with different legend argument.
-
- When histogram parameters are not provided, if an histogram with the
- same legend is displayed in the plot, its parameters are used.
-
- :param numpy.ndarray histogram: The values of the histogram.
- :param numpy.ndarray edges:
- The bin edges of the histogram.
- If histogram and edges have the same length, the bin edges
- are computed according to the align parameter.
- :param str legend:
- The legend to be associated to the histogram (or None)
- :param color: color to be used
- :type color: str ("#RRGGBB") or RGB unsigned byte array or
- one of the predefined color names defined in Colors.py
- :param bool fill: True to fill the curve, False otherwise (default).
- :param str align:
- In case histogram values and edges have the same length N,
- the N+1 bin edges are computed according to the alignment in:
- 'center' (default), 'left', 'right'.
- :param bool resetzoom: True (the default) to reset the zoom.
- :param bool copy: True make a copy of the data (default),
- False to use provided arrays.
- :returns: The key string identify this histogram
- """
- legend = 'Unnamed histogram' if legend is None else str(legend)
-
- # Create/Update histogram object
- histo = self.getHistogram(legend)
- if histo is None:
- # No previous histogram, create a default one and
- # add it to the plot
- histo = items.Histogram()
- histo._setLegend(legend)
- histo.setColor(self._getColorAndStyle()[0])
- self._add(histo)
-
- # Override previous/default values with provided ones
- if color is not None:
- histo.setColor(color)
- if fill is not None:
- histo.setFill(fill)
-
- # Set histogram data
- histo.setData(histogram, edges, align=align, copy=copy)
-
- self.notify(
- 'contentChanged', action='add', kind='histogram', legend=legend)
-
- if resetzoom:
- # We ask for a zoom reset in order to handle the plot scaling
- # if the user does not want that, autoscale of the different
- # axes has to be set to off.
- self.resetZoom()
-
- return legend
-
- def addImage(self, data, legend=None, info=None,
- replace=True, replot=None,
- xScale=None, yScale=None, z=None,
- selectable=None, draggable=None,
- colormap=None, pixmap=None,
- xlabel=None, ylabel=None,
- origin=None, scale=None,
- resetzoom=True, copy=True, **kw):
- """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.
-
- Images are uniquely identified by their legend.
- To add multiple images, call :meth:`addImage` multiple times with
- different legend argument.
- To replace/update an existing image, call :meth:`addImage` with the
- existing image legend.
-
- When image parameters are not provided, if an image with the
- same legend is displayed in the plot, its parameters are used.
-
- :param numpy.ndarray data: (nrows, ncolumns) data or
- (nrows, ncolumns, RGBA) ubyte array
- :param str legend: The legend to be associated to the image (or None)
- :param info: User-defined information associated to the image
- :param bool replace: True (default) to delete already existing images
- :param int z: Layer on which to draw the image (default: 0)
- This allows to control the overlay.
- :param bool selectable: Indicate if the image can be selected.
- (default: False)
- :param bool draggable: Indicate if the image can be moved.
- (default: False)
- :param dict colormap: Description of the colormap to use (or None)
- This is ignored if data is a RGB(A) image.
- See :mod:`Plot` for the documentation
- of the colormap dict.
- :param pixmap: Pixmap representation of the data (if any)
- :type pixmap: (nrows, ncolumns, RGBA) ubyte array or None (default)
- :param str xlabel: X axis label to show when this curve is active,
- or None to keep default axis label.
- :param str ylabel: Y axis label to show when this curve is active,
- or None to keep default axis label.
- :param origin: (origin X, origin Y) of the data.
- It is possible to pass a single float if both
- coordinates are equal.
- Default: (0., 0.)
- :type origin: float or 2-tuple of float
- :param scale: (scale X, scale Y) of the data.
- It is possible to pass a single float if both
- coordinates are equal.
- Default: (1., 1.)
- :type scale: float or 2-tuple of float
- :param bool resetzoom: True (the default) to reset the zoom.
- :param bool copy: True make a copy of the data (default),
- 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
- wasActive = self.getActiveImage(just_legend=True) == legend
-
- data = numpy.array(data, copy=False)
- assert data.ndim in (2, 3)
-
- image = self.getImage(legend)
- if image is not None and image.getData(copy=False).ndim != data.ndim:
- # Update a data image with RGBA image or the other way around:
- # Remove previous image
- # In this case, we don't retrieve defaults from the previous image
- self._remove(image)
- image = None
-
- if image is None:
- # No previous image, create a default one and add it to the plot
- if data.ndim == 2:
- image = items.ImageData()
- image.setColormap(self.getDefaultColormap())
- else:
- image = items.ImageRgba()
- image._setLegend(legend)
- self._add(image)
-
- # Override previous/default values with provided ones
- image.setInfo(info)
- if origin is not None:
- image.setOrigin(origin)
- if scale is not None:
- image.setScale(scale)
- if z is not None:
- image.setZValue(z)
- if selectable is not None:
- image._setSelectable(selectable)
- if draggable is not None:
- image._setDraggable(draggable)
- if colormap is not None and isinstance(image, items.ColormapMixIn):
- image.setColormap(colormap)
- if xlabel is not None:
- image._setXLabel(xlabel)
- if ylabel is not None:
- image._setYLabel(ylabel)
-
- if data.ndim == 2:
- image.setData(data, alternative=pixmap, copy=copy)
- else: # RGB(A) image
- if pixmap is not None:
- _logger.warning(
- 'addImage: pixmap argument ignored when data is RGB(A)')
- image.setData(data, copy=copy)
-
- if replace:
- for img in self.getAllImages():
- if img is not image:
- self._remove(img)
-
- if len(self.getAllImages()) == 1 or wasActive:
- self.setActiveImage(legend)
-
- self.notify(
- 'contentChanged', action='add', kind='image', legend=legend)
-
- if resetzoom:
- # We ask for a zoom reset in order to handle the plot scaling
- # if the user does not want that, autoscale of the different
- # axes has to be set to off.
- self.resetZoom()
-
- return legend
-
- def addScatter(self, x, y, value, legend=None, colormap=None,
- info=None, symbol=None, xerror=None, yerror=None,
- z=None, copy=True):
- """Add a (x, y, value) scatter to the graph.
-
- Scatters are uniquely identified by their legend.
- To add multiple scatters, call :meth:`addScatter` multiple times with
- different legend argument.
- To replace/update an existing scatter, call :meth:`addScatter` with the
- existing scatter legend.
-
- When scatter parameters are not provided, if a scatter with the
- same legend is displayed in the plot, its parameters are used.
-
- :param numpy.ndarray x: The data corresponding to the x coordinates.
- :param numpy.ndarray y: The data corresponding to the y coordinates
- :param numpy.ndarray value: The data value associated with each point
- :param str legend: The legend to be associated to the scatter (or None)
- :param dict colormap: The colormap to be used for the scatter (or None)
- See :mod:`Plot` for the documentation
- of the colormap dict.
- :param info: User-defined information associated to the curve
- :param str symbol: Symbol to be drawn at each (x, y) position::
-
- - 'o' circle
- - '.' point
- - ',' pixel
- - '+' cross
- - 'x' x-cross
- - 'd' diamond
- - 's' square
- - None (the default) to use default symbol
-
- :param xerror: Values with the uncertainties on the x values
- :type xerror: A float, or a numpy.ndarray of float32.
- If it is an array, it can either be a 1D array of
- same length as the data or a 2D array with 2 rows
- of same length as the data: row 0 for positive errors,
- row 1 for negative errors.
- :param yerror: Values with the uncertainties on the y values
- :type yerror: A float, or a numpy.ndarray of float32. See xerror.
- :param int z: Layer on which to draw the scatter (default: 1)
- This allows to control the overlay.
-
- :param bool copy: True make a copy of the data (default),
- False to use provided arrays.
- :returns: The key string identify this scatter
- """
- legend = 'Unnamed scatter 1.1' if legend is None else str(legend)
-
- # Check if scatter was previously active
- wasActive = self._getActiveItem(kind='scatter',
- just_legend=True) == legend
-
- # Create/Update curve object
- scatter = self._getItem(kind='scatter', legend=legend)
- if scatter is None:
- # No previous scatter, create a default one and add it to the plot
- scatter = items.Scatter()
- scatter._setLegend(legend)
- scatter.setColormap(self.getDefaultColormap())
- self._add(scatter)
-
- # Override previous/default values with provided ones
- scatter.setInfo(info)
- if symbol is not None:
- scatter.setSymbol(symbol)
- if z is not None:
- scatter.setZValue(z)
- if colormap is not None:
- scatter.setColormap(colormap)
-
- # Set scatter data
- # If errors not provided, reuse previous ones
- if xerror is None:
- xerror = scatter.getXErrorData(copy=False)
- if xerror is not None and len(xerror) != len(x):
- xerror = None
- if yerror is None:
- yerror = scatter.getYErrorData(copy=False)
- if yerror is not None and len(yerror) != len(y):
- yerror = None
-
- scatter.setData(x, y, value, xerror, yerror, copy=copy)
-
- self.notify(
- 'contentChanged', action='add', kind='scatter', legend=legend)
-
- if len(self._getItems(kind="scatter")) == 1 or wasActive:
- self._setActiveItem('scatter', scatter.getLegend())
-
- return legend
-
- def addItem(self, xdata, ydata, legend=None, info=None,
- replace=False,
- shape="polygon", color='black', fill=True,
- overlay=False, z=None, **kw):
- """Add an item (i.e. a shape) to the plot.
-
- Items are uniquely identified by their legend.
- To add multiple items, call :meth:`addItem` multiple times with
- different legend argument.
- To replace/update an existing item, call :meth:`addItem` with the
- existing item legend.
-
- :param numpy.ndarray xdata: The X coords of the points of the shape
- :param numpy.ndarray ydata: The Y coords of the points of the shape
- :param str legend: The legend to be associated to the item
- :param info: User-defined information associated to the item
- :param bool replace: True (default) to delete already existing images
- :param str shape: Type of item to be drawn in
- hline, polygon (the default), rectangle, vline,
- polylines
- :param str color: Color of the item, e.g., 'blue', 'b', '#FF0000'
- (Default: 'black')
- :param bool fill: True (the default) to fill the shape
- :param bool overlay: True if item is an overlay (Default: False).
- This allows for rendering optimization if this
- item is changed often.
- :param int z: Layer on which to draw the item (default: 2)
- :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
-
- if replace:
- self.remove(kind='item')
- else:
- self.remove(legend, kind='item')
-
- item = items.Shape(shape)
- item._setLegend(legend)
- item.setInfo(info)
- item.setColor(color)
- item.setFill(fill)
- item.setOverlay(overlay)
- item.setZValue(z)
- item.setPoints(numpy.array((xdata, ydata)).T)
-
- self._add(item)
-
- self.notify('contentChanged', action='add', kind='item', legend=legend)
-
- return legend
-
- def addXMarker(self, x, legend=None,
- text=None,
- color=None,
- selectable=False,
- draggable=False,
- constraint=None,
- **kw):
- """Add a vertical line marker to the plot.
-
- Markers are uniquely identified by their legend.
- As opposed to curves, images and items, two calls to
- :meth:`addXMarker` without legend argument adds two markers with
- different identifying legends.
-
- :param float x: Position of the marker on the X axis in data
- coordinates
- :param str legend: Legend associated to the marker to identify it
- :param str text: Text to display on the marker.
- :param str color: Color of the marker, e.g., 'blue', 'b', '#FF0000'
- (Default: 'black')
- :param bool selectable: Indicate if the marker can be selected.
- (default: False)
- :param bool draggable: Indicate if the marker can be moved.
- (default: False)
- :param constraint: A function filtering marker displacement by
- dragging operations or None for no filter.
- This function is called each time a marker is
- moved.
- This parameter is only used if draggable is True.
- :type constraint: None or a callable that takes the coordinates of
- the current cursor position in the plot as input
- 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,
- symbol=None, constraint=constraint)
-
- def addYMarker(self, y,
- legend=None,
- text=None,
- color=None,
- selectable=False,
- draggable=False,
- constraint=None,
- **kw):
- """Add a horizontal line marker to the plot.
-
- Markers are uniquely identified by their legend.
- As opposed to curves, images and items, two calls to
- :meth:`addYMarker` without legend argument adds two markers with
- different identifying legends.
-
- :param float y: Position of the marker on the Y axis in data
- coordinates
- :param str legend: Legend associated to the marker to identify it
- :param str text: Text to display next to the marker.
- :param str color: Color of the marker, e.g., 'blue', 'b', '#FF0000'
- (Default: 'black')
- :param bool selectable: Indicate if the marker can be selected.
- (default: False)
- :param bool draggable: Indicate if the marker can be moved.
- (default: False)
- :param constraint: A function filtering marker displacement by
- dragging operations or None for no filter.
- This function is called each time a marker is
- moved.
- This parameter is only used if draggable is True.
- :type constraint: None or a callable that takes the coordinates of
- the current cursor position in the plot as input
- 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,
- symbol=None, constraint=constraint)
-
- def addMarker(self, x, y, legend=None,
- text=None,
- color=None,
- selectable=False,
- draggable=False,
- symbol='+',
- constraint=None,
- **kw):
- """Add a point marker to the plot.
-
- Markers are uniquely identified by their legend.
- As opposed to curves, images and items, two calls to
- :meth:`addMarker` without legend argument adds two markers with
- different identifying legends.
-
- :param float x: Position of the marker on the X axis in data
- coordinates
- :param float y: Position of the marker on the Y axis in data
- coordinates
- :param str legend: Legend associated to the marker to identify it
- :param str text: Text to display next to the marker
- :param str color: Color of the marker, e.g., 'blue', 'b', '#FF0000'
- (Default: 'black')
- :param bool selectable: Indicate if the marker can be selected.
- (default: False)
- :param bool draggable: Indicate if the marker can be moved.
- (default: False)
- :param str symbol: Symbol representing the marker in::
-
- - 'o' circle
- - '.' point
- - ',' pixel
- - '+' cross (the default)
- - 'x' x-cross
- - 'd' diamond
- - 's' square
-
- :param constraint: A function filtering marker displacement by
- dragging operations or None for no filter.
- This function is called each time a marker is
- moved.
- This parameter is only used if draggable is True.
- :type constraint: None or a callable that takes the coordinates of
- the current cursor position in the plot as input
- 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.getGraphXLimits()
- x = 0.5 * (xmax + xmin)
-
- if y is None:
- ymin, ymax = self.getGraphYLimits()
- y = 0.5 * (ymax + ymin)
-
- return self._addMarker(x=x, y=y, legend=legend,
- text=text, color=color,
- selectable=selectable, draggable=draggable,
- symbol=symbol, constraint=constraint)
-
- def _addMarker(self, x, y, legend,
- text, color,
- selectable, draggable,
- symbol, constraint):
- """Common method for adding point, vline and hline marker.
-
- See :meth:`addMarker` for argument documentation.
- """
- assert (x, y) != (None, None)
-
- if legend is None: # Find an unused legend
- markerLegends = self._getAllMarkers(just_legend=True)
- for index in itertools.count():
- legend = "Unnamed Marker %d" % index
- if legend not in markerLegends:
- break # Keep this legend
- legend = str(legend)
-
- if x is None:
- markerClass = items.YMarker
- elif y is None:
- markerClass = items.XMarker
- else:
- markerClass = items.Marker
-
- # Create/Update marker object
- marker = self._getMarker(legend)
- if marker is not None and not isinstance(marker, markerClass):
- _logger.warning('Adding marker with same legend'
- ' but different type replaces it')
- self._remove(marker)
- marker = None
-
- if marker is None:
- # No previous marker, create one
- marker = markerClass()
- marker._setLegend(legend)
- self._add(marker)
-
- if text is not None:
- marker.setText(text)
- if color is not None:
- marker.setColor(color)
- if selectable is not None:
- marker._setSelectable(selectable)
- if draggable is not None:
- marker._setDraggable(draggable)
- if symbol is not None:
- marker.setSymbol(symbol)
-
- # TODO to improve, but this ensure constraint is applied
- marker.setPosition(x, y)
- if constraint is not None:
- marker._setConstraint(constraint)
- marker.setPosition(x, y)
-
- self.notify(
- 'contentChanged', action='add', kind='marker', legend=legend)
-
- return legend
-
- # Hide
-
- def isCurveHidden(self, legend):
- """Returns True if the curve associated to legend is hidden, else False
-
- :param str legend: The legend key identifying the curve
- :return: True if the associated curve is hidden, False otherwise
- """
- curve = self._getItem('curve', legend)
- return curve is not None and not curve.isVisible()
-
- def hideCurve(self, legend, flag=True, replot=None):
- """Show/Hide the curve associated to legend.
-
- Even when hidden, the curve is kept in the list of curves.
-
- :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)
- return
-
- isVisible = not flag
- if isVisible != curve.isVisible():
- curve.setVisible(isVisible)
-
- # Remove
-
- ITEM_KINDS = 'curve', 'image', 'scatter', 'item', 'marker', 'histogram'
-
- def remove(self, legend=None, kind=ITEM_KINDS):
- """Remove one or all element(s) of the given legend and kind.
-
- Examples:
-
- - ``remove()`` clears the plot
- - ``remove(kind='curve')`` removes all curves from the plot
- - ``remove('myCurve', kind='curve')`` removes the curve with
- legend 'myCurve' from the plot.
- - ``remove('myImage, kind='image')`` removes the image with
- legend 'myImage' from the plot.
- - ``remove('myImage')`` removes elements (for instance curve, image,
- item and marker) with legend 'myImage'.
-
- :param str legend: The legend associated to the element to remove,
- or None to remove
- :param kind: The kind of elements to remove from the plot.
- In: 'all', 'curve', 'image', 'item', 'marker'.
- By default, it removes all kind of elements.
- :type kind: str or tuple of str to specify multiple kinds.
- """
- if kind is 'all': # Replace all by tuple of all kinds
- kind = self.ITEM_KINDS
-
- if kind in self.ITEM_KINDS: # Kind is a str, make it a tuple
- kind = (kind,)
-
- for aKind in kind:
- assert aKind in self.ITEM_KINDS
-
- if legend is None: # This is a clear
- # Clear each given kind
- for aKind in kind:
- for legend in self._getItems(
- kind=aKind, just_legend=True, withhidden=True):
- self.remove(legend=legend, kind=aKind)
-
- else: # This is removing a single element
- # Remove each given kind
- for aKind in kind:
- item = self._getItem(aKind, legend)
- if item is not None:
- if aKind in ('curve', 'image'):
- if self._getActiveItem(aKind) == item:
- # Reset active item
- self._setActiveItem(aKind, None)
-
- self._remove(item)
-
- if (aKind == 'curve' and
- not self.getAllCurves(just_legend=True,
- withhidden=True)):
- self._colorIndex = 0
- self._styleIndex = 0
-
- self.notify('contentChanged', action='remove',
- kind=aKind, legend=legend)
-
- def removeCurve(self, legend):
- """Remove the curve associated to legend from the graph.
-
- :param str legend: The legend associated to the curve to be deleted
- """
- if legend is None:
- return
- self.remove(legend, kind='curve')
-
- def removeImage(self, legend):
- """Remove the image associated to legend from the graph.
-
- :param str legend: The legend associated to the image to be deleted
- """
- if legend is None:
- return
- self.remove(legend, kind='image')
-
- def removeItem(self, legend):
- """Remove the item associated to legend from the graph.
-
- :param str legend: The legend associated to the item to be deleted
- """
- if legend is None:
- return
- self.remove(legend, kind='item')
-
- def removeMarker(self, legend):
- """Remove the marker associated to legend from the graph.
-
- :param str legend: The legend associated to the marker to be deleted
- """
- if legend is None:
- return
- self.remove(legend, kind='marker')
-
- # Clear
-
- def clear(self):
- """Remove everything from the plot."""
- self.remove()
-
- def clearCurves(self):
- """Remove all the curves from the plot."""
- self.remove(kind='curve')
-
- def clearImages(self):
- """Remove all the images from the plot."""
- self.remove(kind='image')
-
- def clearItems(self):
- """Remove all the items from the plot. """
- self.remove(kind='item')
-
- def clearMarkers(self):
- """Remove all the markers from the plot."""
- self.remove(kind='marker')
-
- # Interaction
-
- def getGraphCursor(self):
- """Returns the state of the crosshair cursor.
-
- See :meth:`setGraphCursor`.
-
- :return: None if the crosshair cursor is not active,
- else a tuple (color, linewidth, linestyle).
- """
- return self._cursorConfiguration
-
- def setGraphCursor(self, flag=False, color='black',
- linewidth=1, linestyle='-'):
- """Toggle the display of a crosshair cursor and set its attributes.
-
- :param bool flag: Toggle the display of a crosshair cursor.
- The crosshair cursor is hidden by default.
- :param color: The color to use for the crosshair.
- :type color: A string (either a predefined color name in Colors.py
- or "#RRGGBB")) or a 4 columns unsigned byte array
- (Default: black).
- :param int linewidth: The width of the lines of the crosshair
- (Default: 1).
- :param str linestyle: Type of line::
-
- - ' ' no line
- - '-' solid line (the default)
- - '--' dashed line
- - '-.' dash-dot line
- - ':' dotted line
- """
- if flag:
- self._cursorConfiguration = color, linewidth, linestyle
- else:
- self._cursorConfiguration = None
-
- self._backend.setGraphCursor(flag=flag, color=color,
- linewidth=linewidth, linestyle=linestyle)
- self._setDirtyPlot()
- self.notify('setGraphCursor',
- state=self._cursorConfiguration is not None)
-
- def pan(self, direction, factor=0.1):
- """Pan the graph in the given direction by the given factor.
-
- Warning: Pan of right Y axis not implemented!
-
- :param str direction: One of 'up', 'down', 'left', 'right'.
- :param float factor: Proportion of the range used to pan the graph.
- Must be strictly positive.
- """
- assert direction in ('up', 'down', 'left', 'right')
- assert factor > 0.
-
- if direction in ('left', 'right'):
- xFactor = factor if direction == 'right' else - factor
- xMin, xMax = self.getGraphXLimits()
-
- xMin, xMax = _utils.applyPan(xMin, xMax, xFactor,
- self.isXAxisLogarithmic())
- self.setGraphXLimits(xMin, xMax)
-
- else: # direction in ('up', 'down')
- sign = -1. if self.isYAxisInverted() else 1.
- yFactor = sign * (factor if direction == 'up' else -factor)
- yMin, yMax = self.getGraphYLimits()
- yIsLog = self.isYAxisLogarithmic()
-
- yMin, yMax = _utils.applyPan(yMin, yMax, yFactor, yIsLog)
- self.setGraphYLimits(yMin, yMax, axis='left')
-
- y2Min, y2Max = self.getGraphYLimits(axis='right')
-
- y2Min, y2Max = _utils.applyPan(y2Min, y2Max, yFactor, yIsLog)
- self.setGraphYLimits(y2Min, y2Max, axis='right')
-
- # Active Curve/Image
-
- def isActiveCurveHandling(self):
- """Returns True if active curve selection is enabled."""
- return self._activeCurveHandling
-
- def setActiveCurveHandling(self, flag=True):
- """Enable/Disable active curve selection.
-
- :param bool flag: True (the default) to enable active curve selection.
- """
- if not flag:
- self.setActiveCurve(None) # Reset active curve
-
- self._activeCurveHandling = bool(flag)
-
- def getActiveCurveColor(self):
- """Get the color used to display the currently active curve.
-
- See :meth:`setActiveCurveColor`.
- """
- return self._activeCurveColor
-
- def setActiveCurveColor(self, color="#000000"):
- """Set the color to use to display the currently active curve.
-
- :param str color: Color of the active curve,
- e.g., 'blue', 'b', '#FF0000' (Default: 'black')
- """
- if color is None:
- color = "black"
- if color in self.colorDict:
- color = self.colorDict[color]
- self._activeCurveColor = color
-
- def getActiveCurve(self, just_legend=False):
- """Return the currently active curve.
-
- It returns None in case of not having an active curve.
-
- :param bool just_legend: True to get the legend of the curve,
- False (the default) to get the curve data
- and info.
- :return: Active curve's legend or corresponding
- :class:`.items.Curve`
- :rtype: str or :class:`.items.Curve` or None
- """
- if not self.isActiveCurveHandling():
- return None
-
- return self._getActiveItem(kind='curve', just_legend=just_legend)
-
- def setActiveCurve(self, legend, replot=None):
- """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
-
- return self._setActiveItem(kind='curve', legend=legend)
-
- def getActiveImage(self, just_legend=False):
- """Returns the currently active image.
-
- It returns None in case of not having an active image.
-
- :param bool just_legend: True to get the legend of the image,
- False (the default) to get the image data
- and info.
- :return: Active image's legend or corresponding image object
- :rtype: str, :class:`.items.ImageData`, :class:`.items.ImageRgba`
- or None
- """
- return self._getActiveItem(kind='image', just_legend=just_legend)
-
- def setActiveImage(self, legend, replot=None):
- """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):
- """Return the currently active item of that kind if any
-
- :param str kind: Type of item: 'curve', 'scatter' or 'image'
- :param bool just_legend: True to get the legend,
- False (default) to get the item
- :return: legend or item or None if no active item
- """
- assert kind in ('curve', 'scatter', 'image')
-
- if self._activeLegend[kind] is None:
- return None
-
- if (self._activeLegend[kind], kind) not in self._content:
- self._activeLegend[kind] = None
- return None
-
- if just_legend:
- return self._activeLegend[kind]
- else:
- return self._getItem(kind, self._activeLegend[kind])
-
- def _setActiveItem(self, kind, legend):
- """Make the curve associated to legend the active curve.
-
- :param str kind: Type of item: 'curve' or 'image'
- :param legend: The legend associated to the curve
- or None to have no active curve.
- :type legend: str or None
- """
- assert kind in ('curve', 'image', 'scatter')
-
- xLabel = self._defaultLabels['x']
- yLabel = self._defaultLabels['y']
- yRightLabel = self._defaultLabels['yright']
-
- oldActiveItem = self._getActiveItem(kind=kind)
-
- # Curve specific: Reset highlight of previous active curve
- if kind == 'curve' and oldActiveItem is not None:
- oldActiveItem.setHighlighted(False)
-
- if legend is None:
- self._activeLegend[kind] = None
- else:
- legend = str(legend)
- item = self._getItem(kind, legend)
- if item is None:
- _logger.warning("This %s does not exist: %s", kind, legend)
- self._activeLegend[kind] = None
- else:
- self._activeLegend[kind] = legend
-
- # Curve specific: handle highlight
- if kind == 'curve':
- item.setHighlightedColor(self.getActiveCurveColor())
- item.setHighlighted(True)
-
- if isinstance(item, items.LabelsMixIn):
- if item.getXLabel() is not None:
- xLabel = item.getXLabel()
- if item.getYLabel() is not None:
- if (isinstance(item, items.YAxisMixIn) and
- item.getYAxis() == 'right'):
- yRightLabel = item.getYLabel()
- else:
- yLabel = item.getYLabel()
-
- # Store current labels and update plot
- self._currentLabels['x'] = xLabel
- self._currentLabels['y'] = yLabel
- self._currentLabels['yright'] = yRightLabel
-
- self._backend.setGraphXLabel(xLabel)
- self._backend.setGraphYLabel(yLabel, axis='left')
- self._backend.setGraphYLabel(yRightLabel, axis='right')
-
- self._setDirtyPlot()
-
- activeLegend = self._activeLegend[kind]
- if oldActiveItem is not None or activeLegend is not None:
- if oldActiveItem is None:
- oldActiveLegend = None
- else:
- oldActiveLegend = oldActiveItem.getLegend()
- self.notify(
- 'active' + kind[0].upper() + kind[1:] + 'Changed',
- updated=oldActiveLegend != activeLegend,
- previous=oldActiveLegend,
- legend=activeLegend)
-
- return activeLegend
-
- # Getters
-
- def getAllCurves(self, just_legend=False, withhidden=False):
- """Returns all curves legend or info and data.
-
- It returns an empty list in case of not having any curve.
-
- If just_legend is False, it returns a list of :class:`items.Curve`
- objects describing the curves.
- If just_legend is True, it returns a list of curves' legend.
-
- :param bool just_legend: True to get the legend of the curves,
- False (the default) to get the curves' data
- and info.
- :param bool withhidden: False (default) to skip hidden curves.
- :return: list of curves' legend or :class:`.items.Curve`
- :rtype: list of str or list of :class:`.items.Curve`
- """
- return self._getItems(kind='curve',
- just_legend=just_legend,
- withhidden=withhidden)
-
- def getCurve(self, legend=None):
- """Get the object describing a specific curve.
-
- It returns None in case no matching curve is found.
-
- :param str legend:
- The legend identifying the curve.
- If not provided or None (the default), the active curve is returned
- or if there is no active curve, the latest updated curve that is
- not hidden is returned if there are curves in the plot.
- :return: None or :class:`.items.Curve` object
- """
- return self._getItem(kind='curve', legend=legend)
-
- def getAllImages(self, just_legend=False):
- """Returns all images legend or objects.
-
- It returns an empty list in case of not having any image.
-
- If just_legend is False, it returns a list of :class:`items.ImageBase`
- objects describing the images.
- If just_legend is True, it returns a list of legends.
-
- :param bool just_legend: True to get the legend of the images,
- False (the default) to get the images'
- object.
- :return: list of images' legend or :class:`.items.ImageBase`
- :rtype: list of str or list of :class:`.items.ImageBase`
- """
- return self._getItems(kind='image',
- just_legend=just_legend,
- withhidden=True)
-
- def getImage(self, legend=None):
- """Get the object describing a specific image.
-
- It returns None in case no matching image is found.
-
- :param str legend:
- The legend identifying the image.
- If not provided or None (the default), the active image is returned
- or if there is no active image, the latest updated image
- is returned if there are images in the plot.
- :return: None or :class:`.items.ImageBase` object
- """
- return self._getItem(kind='image', legend=legend)
-
- def getScatter(self, legend=None):
- """Get the object describing a specific scatter.
-
- It returns None in case no matching scatter is found.
-
- :param str legend:
- The legend identifying the scatter.
- If not provided or None (the default), the active scatter is
- returned or if there is no active scatter, the latest updated
- scatter is returned if there are scatters in the plot.
- :return: None or :class:`.items.Scatter` object
- """
- return self._getItem(kind='scatter', legend=legend)
-
- def getHistogram(self, legend=None):
- """Get the object describing a specific histogram.
-
- It returns None in case no matching histogram is found.
-
- :param str legend:
- The legend identifying the histogram.
- If not provided or None (the default), the latest updated scatter
- is returned if there are histograms in the plot.
- :return: None or :class:`.items.Histogram` object
- """
- return self._getItem(kind='histogram', legend=legend)
-
- def _getItems(self, kind, just_legend=False, withhidden=False):
- """Retrieve all items of a kind in the plot
-
- :param str kind: Type of item: 'curve' or 'image'
- :param bool just_legend: True to get the legend of the curves,
- False (the default) to get the curves' data
- and info.
- :param bool withhidden: False (default) to skip hidden curves.
- :return: list of legends or item objects
- """
- assert kind in self.ITEM_KINDS
- output = []
- for (legend, type_), item in self._content.items():
- if type_ == kind and (withhidden or item.isVisible()):
- output.append(legend if just_legend else item)
- return output
-
- def _getItem(self, kind, legend=None):
- """Get an item from the plot: either an image or a curve.
-
- Returns None if no match found
-
- :param str kind: Type of item: 'curve' or 'image'
- :param str legend: Legend of the item or
- None to get active or last item
- :return: Object describing the item or None
- """
- assert kind in self.ITEM_KINDS
-
- if legend is not None:
- return self._content.get((legend, kind), None)
- else:
- if kind in ('curve', 'image', 'scatter'):
- item = self._getActiveItem(kind=kind)
- if item is not None: # Return active item if available
- return item
- # Return last visible item if any
- allItems = self._getItems(
- kind=kind, just_legend=False, withhidden=False)
- return allItems[-1] if allItems else None
-
- # Limits
-
- def _notifyLimitsChanged(self):
- """Send an event when plot area limits are changed."""
- xRange = self.getGraphXLimits()
- yRange = self.getGraphYLimits(axis='left')
- y2Range = self.getGraphYLimits(axis='right')
- event = PlotEvents.prepareLimitsChangedSignal(
- id(self.getWidgetHandle()), xRange, yRange, y2Range)
- self.notify(**event)
-
- def _checkLimits(self, min_, max_, axis):
- """Makes sure axis range is not empty
-
- :param float min_: Min axis value
- :param float max_: Max axis value
- :param str axis: 'x', 'y' or 'y2' the axis to deal with
- :return: (min, max) making sure min < max
- :rtype: 2-tuple of float
- """
- if max_ < min_:
- _logger.info('%s axis: max < min, inverting limits.', axis)
- min_, max_ = max_, min_
- elif max_ == min_:
- _logger.info('%s axis: max == min, expanding limits.', axis)
- if min_ == 0.:
- min_, max_ = -0.1, 0.1
- elif min_ < 0:
- min_, max_ = min_ * 1.1, min_ * 0.9
- else: # xmin > 0
- min_, max_ = min_ * 0.9, min_ * 1.1
-
- return min_, max_
-
- def getGraphXLimits(self):
- """Get the graph X (bottom) limits.
-
- :return: Minimum and maximum values of the X axis
- """
- return self._backend.getGraphXLimits()
-
- def setGraphXLimits(self, xmin, xmax, replot=None):
- """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')
-
- xmin, xmax = self._checkLimits(xmin, xmax, axis='x')
-
- self._backend.setGraphXLimits(xmin, xmax)
- self._setDirtyPlot()
-
- self._notifyLimitsChanged()
-
- def getGraphYLimits(self, axis='left'):
- """Get the graph Y limits.
-
- :param str axis: The axis for which to get the limits:
- Either 'left' or 'right'
- :return: Minimum and maximum values of the X axis
- """
- assert axis in ('left', 'right')
- return self._backend.getGraphYLimits(axis)
-
- def setGraphYLimits(self, ymin, ymax, axis='left', replot=None):
- """Set the graph Y limits.
-
- :param float ymin: minimum bottom axis value
- :param float ymax: maximum bottom axis value
- :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')
-
- ymin, ymax = self._checkLimits(ymin, ymax,
- axis='y' if axis == 'left' else 'y2')
-
- self._backend.setGraphYLimits(ymin, ymax, axis)
- self._setDirtyPlot()
-
- self._notifyLimitsChanged()
-
- def setLimits(self, xmin, xmax, ymin, ymax, y2min=None, y2max=None):
- """Set the limits of the X and Y axes at once.
-
- If y2min or y2max is None, the right Y axis limits are not updated.
-
- :param float xmin: minimum bottom axis value
- :param float xmax: maximum bottom axis value
- :param float ymin: minimum left axis value
- :param float ymax: maximum left axis value
- :param float y2min: minimum right axis value or None (the default)
- :param float y2max: maximum right axis value or None (the default)
- """
- # Deal with incorrect values
- xmin, xmax = self._checkLimits(xmin, xmax, axis='x')
- ymin, ymax = self._checkLimits(ymin, ymax, axis='y')
-
- if y2min is None or y2max is None:
- # if one limit is None, both are ignored
- y2min, y2max = None, None
- else:
- y2min, y2max = self._checkLimits(y2min, y2max, axis='y2')
-
- self._backend.setLimits(xmin, xmax, ymin, ymax, y2min, y2max)
- self._setDirtyPlot()
- self._notifyLimitsChanged()
-
- # Title and labels
-
- def getGraphTitle(self):
- """Return the plot main title as a str."""
- return self._graphTitle
-
- def setGraphTitle(self, title=""):
- """Set the plot main title.
-
- :param str title: Main title of the plot (default: '')
- """
- self._graphTitle = str(title)
- self._backend.setGraphTitle(title)
- self._setDirtyPlot()
-
- def getGraphXLabel(self):
- """Return the current X axis label as a str."""
- return self._currentLabels['x']
-
- def setGraphXLabel(self, label="X"):
- """Set the plot X axis label.
-
- The provided label can be temporarily replaced by the X label of the
- active curve if any.
-
- :param str label: The X axis label (default: 'X')
- """
- self._defaultLabels['x'] = label
- self._currentLabels['x'] = label
- self._backend.setGraphXLabel(label)
- self._setDirtyPlot()
-
- def getGraphYLabel(self, axis='left'):
- """Return the current Y axis label as a str.
-
- :param str axis: The Y axis for which to get the label (left or right)
- """
- assert axis in ('left', 'right')
-
- return self._currentLabels['y' if axis == 'left' else 'yright']
-
- def setGraphYLabel(self, label="Y", axis='left'):
- """Set the plot Y axis label.
-
- The provided label can be temporarily replaced by the Y label of the
- active curve if any.
-
- :param str label: The Y axis label (default: 'Y')
- :param str axis: The Y axis for which to set the label (left or right)
- """
- assert axis in ('left', 'right')
-
- if axis == 'left':
- self._defaultLabels['y'] = label
- self._currentLabels['y'] = label
- else:
- self._defaultLabels['yright'] = label
- self._currentLabels['yright'] = label
-
- self._backend.setGraphYLabel(label, axis=axis)
- self._setDirtyPlot()
-
- # Axes
-
- def setYAxisInverted(self, flag=True):
- """Set the Y axis orientation.
-
- :param bool flag: True for Y axis going from top to bottom,
- False for Y axis going from bottom to top
- """
- flag = bool(flag)
- self._backend.setYAxisInverted(flag)
- self._setDirtyPlot()
- self.notify('setYAxisInverted', state=flag)
-
- def isYAxisInverted(self):
- """Return True if Y axis goes from top to bottom, False otherwise."""
- return self._backend.isYAxisInverted()
-
- def isXAxisLogarithmic(self):
- """Return True if X axis scale is logarithmic, False if linear."""
- return self._logX
-
- def setXAxisLogarithmic(self, flag):
- """Set the bottom X axis scale (either linear or logarithmic).
-
- :param bool flag: True to use a logarithmic scale, False for linear.
- """
- if bool(flag) == self._logX:
- return
- self._logX = bool(flag)
-
- self._backend.setXAxisLogarithmic(self._logX)
-
- # TODO hackish way of forcing update of curves and images
- for curve in self.getAllCurves():
- curve._updated()
- for image in self.getAllImages():
- image._updated()
- self._invalidateDataRange()
-
- self.resetZoom()
- self.notify('setXAxisLogarithmic', state=self._logX)
-
- def isYAxisLogarithmic(self):
- """Return True if Y axis scale is logarithmic, False if linear."""
- return self._logY
-
- def setYAxisLogarithmic(self, flag):
- """Set the Y axes scale (either linear or logarithmic).
-
- :param bool flag: True to use a logarithmic scale, False for linear.
- """
- if bool(flag) == self._logY:
- return
- self._logY = bool(flag)
-
- self._backend.setYAxisLogarithmic(self._logY)
-
- # TODO hackish way of forcing update of curves and images
- for curve in self.getAllCurves():
- curve._updated()
- for image in self.getAllImages():
- image._updated()
- self._invalidateDataRange()
-
- self.resetZoom()
- self.notify('setYAxisLogarithmic', state=self._logY)
-
- def isXAxisAutoScale(self):
- """Return True if X axis is automatically adjusting its limits."""
- return self._xAutoScale
-
- def setXAxisAutoScale(self, flag=True):
- """Set the X axis limits adjusting behavior of :meth:`resetZoom`.
-
- :param bool flag: True to resize limits automatically,
- False to disable it.
- """
- self._xAutoScale = bool(flag)
- self.notify('setXAxisAutoScale', state=self._xAutoScale)
-
- def isYAxisAutoScale(self):
- """Return True if Y axes are automatically adjusting its limits."""
- return self._yAutoScale
-
- def setYAxisAutoScale(self, flag=True):
- """Set the Y axis limits adjusting behavior of :meth:`resetZoom`.
-
- :param bool flag: True to resize limits automatically,
- False to disable it.
- """
- self._yAutoScale = bool(flag)
- self.notify('setYAxisAutoScale', state=self._yAutoScale)
-
- def isKeepDataAspectRatio(self):
- """Returns whether the plot is keeping data aspect ratio or not."""
- return self._backend.isKeepDataAspectRatio()
-
- def setKeepDataAspectRatio(self, flag=True):
- """Set whether the plot keeps data aspect ratio or not.
-
- :param bool flag: True to respect data aspect ratio
- """
- flag = bool(flag)
- self._backend.setKeepDataAspectRatio(flag=flag)
- self._setDirtyPlot()
- self.resetZoom()
- self.notify('setKeepDataAspectRatio', state=flag)
-
- def getGraphGrid(self):
- """Return the current grid mode, either None, 'major' or 'both'.
-
- See :meth:`setGraphGrid`.
- """
- return self._grid
-
- def setGraphGrid(self, which=True):
- """Set the type of grid to display.
-
- :param which: None or False to disable the grid,
- 'major' or True for grid on major ticks (the default),
- 'both' for grid on both major and minor ticks.
- :type which: str of bool
- """
- assert which in (None, True, False, 'both', 'major')
- if not which:
- which = None
- elif which is True:
- which = 'major'
- self._grid = which
- self._backend.setGraphGrid(which)
- self._setDirtyPlot()
- self.notify('setGraphGrid', which=str(which))
-
- # Defaults
-
- def isDefaultPlotPoints(self):
- """Return True if default Curve symbol is 'o', False for no symbol."""
- return self._defaultPlotPoints == 'o'
-
- def setDefaultPlotPoints(self, flag):
- """Set the default symbol of all curves.
-
- When called, this reset the symbol of all existing curves.
-
- :param bool flag: True to use 'o' as the default curve symbol,
- False to use no symbol.
- """
- self._defaultPlotPoints = 'o' if flag else ''
-
- # Reset symbol of all curves
- curves = self.getAllCurves(just_legend=False, withhidden=True)
-
- if curves:
- for curve in curves:
- curve.setSymbol(self._defaultPlotPoints)
-
- def isDefaultPlotLines(self):
- """Return True for line as default line style, False for no line."""
- return self._plotLines
-
- def setDefaultPlotLines(self, flag):
- """Toggle the use of lines as the default curve line style.
-
- :param bool flag: True to use a line as the default line style,
- False to use no line as the default line style.
- """
- self._plotLines = bool(flag)
-
- linestyle = '-' if self._plotLines else ' '
-
- # Reset linestyle of all curves
- curves = self.getAllCurves(withhidden=True)
-
- if curves:
- for curve in curves:
- curve.setLineStyle(linestyle)
-
- def getDefaultColormap(self):
- """Return the default colormap used by :meth:`addImage` as a dict.
-
- See :mod:`Plot` for the documentation of the colormap dict.
- """
- return self._defaultColormap.copy()
-
- def setDefaultColormap(self, colormap=None):
- """Set the default colormap used by :meth:`addImage`.
-
- Setting the default colormap do not change any currently displayed
- image.
- It only affects future calls to :meth:`addImage` without the colormap
- parameter.
-
- :param dict colormap: The description of the default colormap, or
- None to set the colormap to a linear autoscale
- gray colormap.
- See :mod:`Plot` for the documentation
- of the colormap dict.
- """
- if colormap is None:
- colormap = {'name': 'gray', 'normalization': 'linear',
- 'autoscale': True, 'vmin': 0.0, 'vmax': 1.0}
- self._defaultColormap = colormap.copy()
-
- def getSupportedColormaps(self):
- """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')
- """
- default = ('gray', 'reversed gray',
- 'temperature',
- 'red', 'green', 'blue')
- if matplotlib_cm is None:
- return default
- else:
- maps = [m for m in matplotlib_cm.datad]
- maps.sort()
- return default + tuple(maps)
-
- def _getColorAndStyle(self):
- color = self.colorList[self._colorIndex]
- style = self._styleList[self._styleIndex]
-
- # Loop over color and then styles
- self._colorIndex += 1
- if self._colorIndex >= len(self.colorList):
- self._colorIndex = 0
- self._styleIndex = (self._styleIndex + 1) % len(self._styleList)
-
- # If color is the one of active curve, take the next one
- if color == self.getActiveCurveColor():
- color, style = self._getColorAndStyle()
-
- if not self._plotLines:
- style = ' '
-
- return color, style
-
- # Misc.
-
- def getWidgetHandle(self):
- """Return the widget the plot is displayed in.
-
- This widget is owned by the backend.
- """
- return self._backend.getWidgetHandle()
-
- def notify(self, event, **kwargs):
- """Send an event to the listeners.
-
- Event are passed to the registered callback as a dict with an 'event'
- key for backward compatibility with PyMca.
-
- :param str event: The type of event
- :param kwargs: The information of the event.
- """
- eventDict = kwargs.copy()
- eventDict['event'] = event
- self._callback(eventDict)
-
- def setCallback(self, callbackFunction=None):
- """Attach a listener to the backend.
-
- Limitation: Only one listener at a time.
-
- :param callbackFunction: function accepting a dictionary as input
- to handle the graph events
- If None (default), use a default listener.
- """
- # TODO allow multiple listeners, keep a weakref on it
- # allow register listener by event type
- if callbackFunction is None:
- callbackFunction = self.graphCallback
- self._callback = callbackFunction
-
- def graphCallback(self, ddict=None):
- """This callback is going to receive all the events from the plot.
-
- Those events will consist on a dictionary and among the dictionary
- keys the key 'event' is mandatory to describe the type of event.
- This default implementation only handles setting the active curve.
- """
-
- if ddict is None:
- ddict = {}
- _logger.debug("Received dict keys = %s", str(ddict.keys()))
- _logger.debug(str(ddict))
- if ddict['event'] in ["legendClicked", "curveClicked"]:
- if ddict['button'] == "left":
- self.setActiveCurve(ddict['label'])
-
- def saveGraph(self, filename, fileFormat=None, dpi=None, **kw):
- """Save a snapshot of the plot.
-
- Supported file formats: "png", "svg", "pdf", "ps", "eps",
- "tif", "tiff", "jpeg", "jpg".
-
- :param filename: Destination
- :type filename: str, StringIO or BytesIO
- :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(
- 'saveGraph cancelled, cannot define file format.')
- return False
- else:
- fileFormat = (filename.split(".")[-1]).lower()
-
- supportedFormats = ("png", "svg", "pdf", "ps", "eps",
- "tif", "tiff", "jpeg", "jpg")
-
- if fileFormat not in supportedFormats:
- _logger.warning('Unsupported format %s', fileFormat)
- return False
- else:
- self._backend.saveGraph(filename,
- fileFormat=fileFormat,
- dpi=dpi)
- return True
-
- def getDataMargins(self):
- """Get the default data margin ratios, see :meth:`setDataMargins`.
-
- :return: The margin ratios for each side (xMin, xMax, yMin, yMax).
- :rtype: A 4-tuple of floats.
- """
- return self._defaultDataMargins
-
- def setDataMargins(self, xMinMargin=0., xMaxMargin=0.,
- yMinMargin=0., yMaxMargin=0.):
- """Set the default data margins to use in :meth:`resetZoom`.
-
- Set the default ratios of margins (as floats) to add around the data
- inside the plot area for each side.
- """
- self._defaultDataMargins = (xMinMargin, xMaxMargin,
- yMinMargin, yMaxMargin)
-
- def getAutoReplot(self):
- """Return True if replot is automatically handled, False otherwise.
-
- See :meth`setAutoReplot`.
- """
- return self._autoreplot
-
- def setAutoReplot(self, autoreplot=True):
- """Set automatic replot mode.
-
- When enabled, the plot is redrawn automatically when changed.
- When disabled, the plot is not redrawn when its content change.
- Instead, it :meth:`replot` must be called.
-
- :param bool autoreplot: True to enable it (default),
- False to disable it.
- """
- self._autoreplot = bool(autoreplot)
-
- # If the plot is dirty before enabling autoreplot,
- # then _backend.postRedisplay will never be called from _setDirtyPlot
- if self._autoreplot and self._getDirtyPlot():
- self._backend.postRedisplay()
-
- def replot(self):
- """Redraw the plot immediately."""
- for item in self._contentToUpdate:
- item._update(self._backend)
- self._contentToUpdate.clear()
- self._backend.replot()
- self._dirty = False # reset dirty flag
-
- def resetZoom(self, dataMargins=None):
- """Reset the plot limits to the bounds of the data and redraw the plot.
-
- It automatically scale limits of axes that are in autoscale mode
- (See :meth:`setXAxisAutoScale`, :meth:`setYAxisAutoScale`).
- It keeps current limits on axes that are not in autoscale mode.
-
- Extra margins can be added around the data inside the plot area.
- Margins are given as one ratio of the data range per limit of the
- data (xMin, xMax, yMin and yMax limits).
- For log scale, extra margins are applied in log10 of the data.
-
- :param dataMargins: Ratios of margins to add around the data inside
- the plot area for each side (Default: no margins).
- :type dataMargins: A 4-tuple of float as (xMin, xMax, yMin, yMax).
- """
- if dataMargins is None:
- dataMargins = self._defaultDataMargins
-
- xLimits = self.getGraphXLimits()
- yLimits = self.getGraphYLimits(axis='left')
- y2Limits = self.getGraphYLimits(axis='right')
-
- xAuto = self.isXAxisAutoScale()
- yAuto = self.isYAxisAutoScale()
-
- if not xAuto and not yAuto:
- _logger.debug("Nothing to autoscale")
- else: # Some axes to autoscale
-
- # Get data range
- ranges = self.getDataRange()
- 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
- else:
- ymin2, ymax2 = ranges.yright
-
- # Add margins around data inside the plot area
- newLimits = list(_utils.addMarginsToLimits(
- dataMargins,
- self.isXAxisLogarithmic(),
- self.isYAxisLogarithmic(),
- xmin, xmax, ymin, ymax, ymin2, ymax2))
-
- if self.isKeepDataAspectRatio():
- # Use limits with margins to keep ratio
- xmin, xmax, ymin, ymax = newLimits[:4]
-
- # Compute bbox wth figure aspect ratio
- plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
- plotRatio = plotHeight / plotWidth
-
- if plotRatio > 0.:
- dataRatio = (ymax - ymin) / (xmax - xmin)
- if dataRatio < plotRatio:
- # Increase y range
- ycenter = 0.5 * (ymax + ymin)
- yrange = (xmax - xmin) * plotRatio
- newLimits[2] = ycenter - 0.5 * yrange
- newLimits[3] = ycenter + 0.5 * yrange
-
- elif dataRatio > plotRatio:
- # Increase x range
- xcenter = 0.5 * (xmax + xmin)
- xrange_ = (ymax - ymin) / plotRatio
- newLimits[0] = xcenter - 0.5 * xrange_
- newLimits[1] = xcenter + 0.5 * xrange_
-
- self.setLimits(*newLimits)
-
- if not xAuto and yAuto:
- self.setGraphXLimits(*xLimits)
- elif xAuto and not yAuto:
- if y2Limits is not None:
- self.setGraphYLimits(
- y2Limits[0], y2Limits[1], axis='right')
- if yLimits is not None:
- self.setGraphYLimits(yLimits[0], yLimits[1], axis='left')
-
- self._setDirtyPlot()
-
- if (xLimits != self.getGraphXLimits() or
- yLimits != self.getGraphYLimits(axis='left') or
- y2Limits != self.getGraphYLimits(axis='right')):
- self._notifyLimitsChanged()
-
- # Coord conversion
-
- def dataToPixel(self, x=None, y=None, axis="left", check=True):
- """Convert a position in data coordinates to a position in pixels.
-
- :param float x: The X coordinate in data space. If None (default)
- the middle position of the displayed data is used.
- :param float y: The Y coordinate in data space. If None (default)
- the middle position of the displayed data is used.
- :param str axis: The Y axis to use for the conversion
- ('left' or 'right').
- :param bool check: True to return None if outside displayed area,
- False to convert to pixels anyway
- :returns: The corresponding position in pixels or
- None if the data position is not in the displayed area and
- check is True.
- :rtype: A tuple of 2 floats: (xPixel, yPixel) or None.
- """
- assert axis in ("left", "right")
-
- xmin, xmax = self.getGraphXLimits()
- ymin, ymax = self.getGraphYLimits(axis=axis)
-
- if x is None:
- x = 0.5 * (xmax + xmin)
- if y is None:
- y = 0.5 * (ymax + ymin)
-
- if check:
- if x > xmax or x < xmin:
- return None
-
- if y > ymax or y < ymin:
- return None
-
- return self._backend.dataToPixel(x, y, axis=axis)
-
- def pixelToData(self, x, y, axis="left", check=False):
- """Convert a position in pixels to a position in data coordinates.
-
- :param float x: The X coordinate in pixels. If None (default)
- the center of the widget is used.
- :param float y: The Y coordinate in pixels. If None (default)
- the center of the widget is used.
- :param str axis: The Y axis to use for the conversion
- ('left' or 'right').
- :param bool check: Toggle checking if pixel is in plot area.
- If False, this method never returns None.
- :returns: The corresponding position in data space or
- None if the pixel position is not in the plot area.
- :rtype: A tuple of 2 floats: (xData, yData) or None.
- """
- assert axis in ("left", "right")
- return self._backend.pixelToData(x, y, axis=axis, check=check)
-
- def getPlotBoundsInPixels(self):
- """Plot area bounds in widget coordinates in pixels.
-
- :return: bounds as a 4-tuple of int: (left, top, width, height)
- """
- return self._backend.getPlotBoundsInPixels()
-
- # Interaction support
-
- def setGraphCursorShape(self, cursor=None):
- """Set the cursor shape.
-
- :param str cursor: Name of the cursor shape
- """
- self._backend.setGraphCursorShape(cursor)
-
- def _pickMarker(self, x, y, test=None):
- """Pick a marker at the given position.
-
- To use for interaction implementation.
-
- :param float x: X position in pixels.
- :param float y: Y position in pixels.
- :param test: A callable to call for each picked marker to filter
- picked markers. If None (default), do not filter markers.
- """
- if test is None:
- def test(mark):
- return True
-
- markers = self._backend.pickItems(x, y)
- legends = [m['legend'] for m in markers if m['kind'] == 'marker']
-
- for legend in reversed(legends):
- marker = self._getMarker(legend)
- if marker is not None and test(marker):
- return marker
- return None
-
- def _getAllMarkers(self, just_legend=False):
- """Returns all markers' legend or objects
-
- :param bool just_legend: True to get the legend of the markers,
- False (the default) to get marker objects.
- :return: list of legend of list of marker objects
- :rtype: list of str or list of marker objects
- """
- return self._getItems(
- kind='marker', just_legend=just_legend, withhidden=True)
-
- def _getMarker(self, legend=None):
- """Get the object describing a specific marker.
-
- It returns None in case no matching marker is found
-
- :param str legend: The legend of the marker to retrieve
- :rtype: None of marker object
- """
- return self._getItem(kind='marker', legend=legend)
-
- def _pickImageOrCurve(self, x, y, test=None):
- """Pick an image or a curve at the given position.
-
- To use for interaction implementation.
-
- :param float x: X position in pixelsparam float y: Y position in pixels
- :param test: A callable to call for each picked item to filter
- picked items. If None (default), do not filter items.
- """
- if test is None:
- def test(i):
- return True
-
- allItems = self._backend.pickItems(x, y)
- allItems = [item for item in allItems
- if item['kind'] in ['curve', 'image']]
-
- for item in reversed(allItems):
- kind, legend = item['kind'], item['legend']
- if kind == 'curve':
- curve = self.getCurve(legend)
- if curve is not None and test(curve):
- return kind, curve, item['xdata'], item['ydata']
-
- elif kind == 'image':
- image = self.getImage(legend)
- if image is not None and test(image):
- return kind, image, None
-
- else:
- _logger.warning('Unsupported kind: %s', kind)
-
- return None
-
- # User event handling #
-
- def _isPositionInPlotArea(self, x, y):
- """Project position in pixel to the closest point in the plot area
-
- :param float x: X coordinate in widget coordinate (in pixel)
- :param float y: Y coordinate in widget coordinate (in pixel)
- :return: (x, y) in widget coord (in pixel) in the plot area
- """
- left, top, width, height = self.getPlotBoundsInPixels()
- xPlot = numpy.clip(x, left, left + width)
- yPlot = numpy.clip(y, top, top + height)
- return xPlot, yPlot
-
- def onMousePress(self, xPixel, yPixel, btn):
- """Handle mouse press event.
-
- :param float xPixel: X mouse position in pixels
- :param float yPixel: Y mouse position in pixels
- :param str btn: Mouse button in 'left', 'middle', 'right'
- """
- if self._isPositionInPlotArea(xPixel, yPixel) == (xPixel, yPixel):
- self._pressedButtons.append(btn)
- self._eventHandler.handleEvent('press', xPixel, yPixel, btn)
-
- def onMouseMove(self, xPixel, yPixel):
- """Handle mouse move event.
-
- :param float xPixel: X mouse position in pixels
- :param float yPixel: Y mouse position in pixels
- """
- inXPixel, inYPixel = self._isPositionInPlotArea(xPixel, yPixel)
- isCursorInPlot = inXPixel == xPixel and inYPixel == yPixel
-
- if self._cursorInPlot != isCursorInPlot:
- self._cursorInPlot = isCursorInPlot
- self._eventHandler.handleEvent(
- 'enter' if self._cursorInPlot else 'leave')
-
- if isCursorInPlot:
- # Signal mouse move event
- dataPos = self.pixelToData(inXPixel, inYPixel)
- assert dataPos is not None
-
- btn = self._pressedButtons[-1] if self._pressedButtons else None
- event = PlotEvents.prepareMouseSignal(
- 'mouseMoved', btn, dataPos[0], dataPos[1], xPixel, yPixel)
- self.notify(**event)
-
- # Either button was pressed in the plot or cursor is in the plot
- if isCursorInPlot or self._pressedButtons:
- self._eventHandler.handleEvent('move', inXPixel, inYPixel)
-
- def onMouseRelease(self, xPixel, yPixel, btn):
- """Handle mouse release event.
-
- :param float xPixel: X mouse position in pixels
- :param float yPixel: Y mouse position in pixels
- :param str btn: Mouse button in 'left', 'middle', 'right'
- """
- try:
- self._pressedButtons.remove(btn)
- except ValueError:
- pass
- else:
- xPixel, yPixel = self._isPositionInPlotArea(xPixel, yPixel)
- self._eventHandler.handleEvent('release', xPixel, yPixel, btn)
-
- def onMouseWheel(self, xPixel, yPixel, angleInDegrees):
- """Handle mouse wheel event.
-
- :param float xPixel: X mouse position in pixels
- :param float yPixel: Y mouse position in pixels
- :param float angleInDegrees: Angle corresponding to wheel motion.
- Positive for movement away from the user,
- negative for movement toward the user.
- """
- if self._isPositionInPlotArea(xPixel, yPixel) == (xPixel, yPixel):
- self._eventHandler.handleEvent(
- 'wheel', xPixel, yPixel, angleInDegrees)
-
- def onMouseLeaveWidget(self):
- """Handle mouse leave widget event."""
- if self._cursorInPlot:
- self._cursorInPlot = False
- self._eventHandler.handleEvent('leave')
-
- # Interaction modes #
-
- def getInteractiveMode(self):
- """Returns the current interactive mode as a dict.
-
- The returned dict contains at least the key 'mode'.
- Mode can be: 'draw', 'pan', 'select', 'zoom'.
- It can also contains extra keys (e.g., 'color') specific to a mode
- as provided to :meth:`setInteractiveMode`.
- """
- return self._eventHandler.getInteractiveMode()
-
- def setInteractiveMode(self, mode, color='black',
- shape='polygon', label=None,
- zoomOnWheel=True, source=None, width=None):
- """Switch the interactive mode.
-
- :param str mode: The name of the interactive mode.
- In 'draw', 'pan', 'select', 'zoom'.
- :param color: Only for 'draw' and 'zoom' modes.
- Color to use for drawing selection area. Default black.
- :type color: Color description: The name as a str or
- a tuple of 4 floats.
- :param str shape: Only for 'draw' mode. The kind of shape to draw.
- In 'polygon', 'rectangle', 'line', 'vline', 'hline',
- 'freeline'.
- Default is 'polygon'.
- :param str label: Only for 'draw' mode, sent in drawing events.
- :param bool zoomOnWheel: Toggle zoom on wheel support
- :param source: A user-defined object (typically the caller object)
- that will be send in the interactiveModeChanged event,
- to identify which object required a mode change.
- Default: None
- :param float width: Width of the pencil. Only for draw pencil mode.
- """
- self._eventHandler.setInteractiveMode(mode, color, shape, label, width)
- self._eventHandler.zoomOnWheel = zoomOnWheel
-
- self.notify(
- 'interactiveModeChanged', source=source)
-
- # 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:`setYAxisInverted` instead."""
- _logger.warning('invertYAxis deprecated, '
- 'use setYAxisInverted instead.')
- return self.setYAxisInverted(*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/PlotActions.py b/silx/gui/plot/PlotActions.py
index aad27d2..dd16221 100644
--- a/silx/gui/plot/PlotActions.py
+++ b/silx/gui/plot/PlotActions.py
@@ -22,1365 +22,46 @@
# THE SOFTWARE.
#
# ###########################################################################*/
-"""This module provides a set of QAction to use with :class:`.PlotWidget`.
+"""Depracted module linking old PlotAction with the actions.xxx"""
-The following QAction are available:
-- :class:`ColormapAction`
-- :class:`CopyAction`
-- :class:`CrosshairAction`
-- :class:`CurveStyleAction`
-- :class:`FitAction`
-- :class:`GridAction`
-- :class:`KeepAspectRatioAction`
-- :class:`PanWithArrowKeysAction`
-- :class:`PrintAction`
-- :class:`ResetZoomAction`
-- :class:`SaveAction`
-- :class:`XAxisLogarithmicAction`
-- :class:`XAxisAutoScaleAction`
-- :class:`YAxisInvertedAction`
-- :class:`YAxisLogarithmicAction`
-- :class:`YAxisAutoScaleAction`
-- :class:`ZoomInAction`
-- :class:`ZoomOutAction`
-"""
-
-from __future__ import division
-
-
-__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
+__author__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
-__date__ = "20/04/2017"
-
-
-from collections import OrderedDict
-import logging
-import sys
-import traceback
-import weakref
-
-if sys.version_info[0] == 3:
- from io import BytesIO
-else:
- import cStringIO as _StringIO
- BytesIO = _StringIO.StringIO
-
-import numpy
-
-from .. import icons
-from .. import qt
-from .._utils import convertArrayToQImage
-from . import Colors, items
-from .ColormapDialog import ColormapDialog
-from ._utils import applyZoomToPlot as _applyZoomToPlot
-from silx.third_party.EdfFile import EdfFile
-from silx.third_party.TiffIO import TiffIO
-from silx.math.histogram import Histogramnd
-from silx.math.medianfilter import medfilt2d
-from silx.gui.widgets.MedianFilterDialog import MedianFilterDialog
-
-from silx.io.utils import save1D, savespec
-
-
-_logger = logging.getLogger(__name__)
-
-
-class PlotAction(qt.QAction):
- """Base class for QAction that operates on a PlotWidget.
-
- :param plot: :class:`.PlotWidget` instance on which to operate.
- :param icon: QIcon or str name of icon to use
- :param str text: The name of this action to be used for menu label
- :param str tooltip: The text of the tooltip
- :param triggered: The callback to connect to the action's triggered
- signal or None for no callback.
- :param bool checkable: True for checkable action, False otherwise (default)
- :param parent: See :class:`QAction`.
- """
-
- def __init__(self, plot, icon, text, tooltip=None,
- triggered=None, checkable=False, parent=None):
- assert plot is not None
- self._plotRef = weakref.ref(plot)
-
- if not isinstance(icon, qt.QIcon):
- # Try with icon as a string and load corresponding icon
- icon = icons.getQIcon(icon)
-
- super(PlotAction, self).__init__(icon, text, parent)
-
- if tooltip is not None:
- self.setToolTip(tooltip)
-
- self.setCheckable(checkable)
-
- if triggered is not None:
- self.triggered[bool].connect(triggered)
-
- @property
- def plot(self):
- """The :class:`.PlotWidget` this action group is controlling."""
- return self._plotRef()
-
-
-class ResetZoomAction(PlotAction):
- """QAction controlling reset zoom on a :class:`.PlotWidget`.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- super(ResetZoomAction, self).__init__(
- plot, icon='zoom-original', text='Reset Zoom',
- tooltip='Auto-scale the graph',
- triggered=self._actionTriggered,
- checkable=False, parent=parent)
- self._autoscaleChanged(True)
- plot.sigSetXAxisAutoScale.connect(self._autoscaleChanged)
- plot.sigSetYAxisAutoScale.connect(self._autoscaleChanged)
-
- def _autoscaleChanged(self, enabled):
- self.setEnabled(
- self.plot.isXAxisAutoScale() or self.plot.isYAxisAutoScale())
-
- if self.plot.isXAxisAutoScale() and self.plot.isYAxisAutoScale():
- tooltip = 'Auto-scale the graph'
- elif self.plot.isXAxisAutoScale(): # And not Y axis
- tooltip = 'Auto-scale the x-axis of the graph only'
- elif self.plot.isYAxisAutoScale(): # And not X axis
- tooltip = 'Auto-scale the y-axis of the graph only'
- else: # no axis in autoscale
- tooltip = 'Auto-scale the graph'
- self.setToolTip(tooltip)
-
- def _actionTriggered(self, checked=False):
- self.plot.resetZoom()
-
-
-class ZoomInAction(PlotAction):
- """QAction performing a zoom-in on a :class:`.PlotWidget`.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- super(ZoomInAction, self).__init__(
- plot, icon='zoom-in', text='Zoom In',
- tooltip='Zoom in the plot',
- triggered=self._actionTriggered,
- checkable=False, parent=parent)
- self.setShortcut(qt.QKeySequence.ZoomIn)
- self.setShortcutContext(qt.Qt.WidgetShortcut)
-
- def _actionTriggered(self, checked=False):
- _applyZoomToPlot(self.plot, 1.1)
-
-
-class ZoomOutAction(PlotAction):
- """QAction performing a zoom-out on a :class:`.PlotWidget`.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- super(ZoomOutAction, self).__init__(
- plot, icon='zoom-out', text='Zoom Out',
- tooltip='Zoom out the plot',
- triggered=self._actionTriggered,
- checkable=False, parent=parent)
- self.setShortcut(qt.QKeySequence.ZoomOut)
- self.setShortcutContext(qt.Qt.WidgetShortcut)
-
- def _actionTriggered(self, checked=False):
- _applyZoomToPlot(self.plot, 1. / 1.1)
-
-
-class XAxisAutoScaleAction(PlotAction):
- """QAction controlling X axis autoscale on a :class:`.PlotWidget`.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- super(XAxisAutoScaleAction, self).__init__(
- plot, icon='plot-xauto', text='X Autoscale',
- tooltip='Enable x-axis auto-scale when checked.\n'
- 'If unchecked, x-axis does not change when reseting zoom.',
- triggered=self._actionTriggered,
- checkable=True, parent=parent)
- self.setChecked(plot.isXAxisAutoScale())
- plot.sigSetXAxisAutoScale.connect(self.setChecked)
-
- def _actionTriggered(self, checked=False):
- self.plot.setXAxisAutoScale(checked)
- if checked:
- self.plot.resetZoom()
-
-
-class YAxisAutoScaleAction(PlotAction):
- """QAction controlling Y axis autoscale on a :class:`.PlotWidget`.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- super(YAxisAutoScaleAction, self).__init__(
- plot, icon='plot-yauto', text='Y Autoscale',
- tooltip='Enable y-axis auto-scale when checked.\n'
- 'If unchecked, y-axis does not change when reseting zoom.',
- triggered=self._actionTriggered,
- checkable=True, parent=parent)
- self.setChecked(plot.isXAxisAutoScale())
- plot.sigSetYAxisAutoScale.connect(self.setChecked)
-
- def _actionTriggered(self, checked=False):
- self.plot.setYAxisAutoScale(checked)
- if checked:
- self.plot.resetZoom()
-
-
-class XAxisLogarithmicAction(PlotAction):
- """QAction controlling X axis log scale on a :class:`.PlotWidget`.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- super(XAxisLogarithmicAction, self).__init__(
- plot, icon='plot-xlog', text='X Log. scale',
- tooltip='Logarithmic x-axis when checked',
- triggered=self._actionTriggered,
- checkable=True, parent=parent)
- self.setChecked(plot.isXAxisLogarithmic())
- plot.sigSetXAxisLogarithmic.connect(self.setChecked)
-
- def _actionTriggered(self, checked=False):
- self.plot.setXAxisLogarithmic(checked)
-
-
-class YAxisLogarithmicAction(PlotAction):
- """QAction controlling Y axis log scale on a :class:`.PlotWidget`.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- super(YAxisLogarithmicAction, self).__init__(
- plot, icon='plot-ylog', text='Y Log. scale',
- tooltip='Logarithmic y-axis when checked',
- triggered=self._actionTriggered,
- checkable=True, parent=parent)
- self.setChecked(plot.isYAxisLogarithmic())
- plot.sigSetYAxisLogarithmic.connect(self.setChecked)
-
- def _actionTriggered(self, checked=False):
- self.plot.setYAxisLogarithmic(checked)
-
-
-class GridAction(PlotAction):
- """QAction controlling grid mode on a :class:`.PlotWidget`.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param str gridMode: The grid mode to use in 'both', 'major'.
- See :meth:`.PlotWidget.setGraphGrid`
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, gridMode='both', parent=None):
- assert gridMode in ('both', 'major')
- self._gridMode = gridMode
-
- super(GridAction, self).__init__(
- plot, icon='plot-grid', text='Grid',
- tooltip='Toggle grid (on/off)',
- triggered=self._actionTriggered,
- checkable=True, parent=parent)
- self.setChecked(plot.getGraphGrid() is not None)
- plot.sigSetGraphGrid.connect(self._gridChanged)
-
- def _gridChanged(self, which):
- """Slot listening for PlotWidget grid mode change."""
- self.setChecked(which != 'None')
-
- def _actionTriggered(self, checked=False):
- self.plot.setGraphGrid(self._gridMode if checked else None)
-
-
-class CurveStyleAction(PlotAction):
- """QAction controlling curve style on a :class:`.PlotWidget`.
-
- It changes the default line and markers style which updates all
- curves on the plot.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- super(CurveStyleAction, self).__init__(
- plot, icon='plot-toggle-points', text='Curve style',
- tooltip='Change curve line and markers style',
- triggered=self._actionTriggered,
- checkable=False, parent=parent)
-
- def _actionTriggered(self, checked=False):
- currentState = (self.plot.isDefaultPlotLines(),
- self.plot.isDefaultPlotPoints())
-
- # line only, line and symbol, symbol only
- states = (True, False), (True, True), (False, True)
- newState = states[(states.index(currentState) + 1) % 3]
-
- self.plot.setDefaultPlotLines(newState[0])
- self.plot.setDefaultPlotPoints(newState[1])
-
-
-class ColormapAction(PlotAction):
- """QAction opening a ColormapDialog to update the colormap.
-
- Both the active image colormap and the default colormap are updated.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
- def __init__(self, plot, parent=None):
- self._dialog = None # To store an instance of ColormapDialog
- super(ColormapAction, self).__init__(
- plot, icon='colormap', text='Colormap',
- tooltip="Change colormap",
- triggered=self._actionTriggered,
- checkable=False, parent=parent)
-
- def _actionTriggered(self, checked=False):
- """Create a cmap dialog and update active image and default cmap."""
- # Create the dialog if not already existing
- if self._dialog is None:
- self._dialog = ColormapDialog()
-
- image = self.plot.getActiveImage()
- if not isinstance(image, items.ColormapMixIn):
- # No active image or active image is RGBA,
- # set dialog from default info
- colormap = self.plot.getDefaultColormap()
-
- self._dialog.setHistogram() # Reset histogram and range if any
-
- else:
- # Set dialog from active image
- colormap = image.getColormap()
-
- data = image.getData(copy=False)
-
- goodData = data[numpy.isfinite(data)]
- if goodData.size > 0:
- dataMin = goodData.min()
- dataMax = goodData.max()
- else:
- qt.QMessageBox.warning(
- self, "No Data",
- "Image data does not contain any real value")
- dataMin, dataMax = 1., 10.
-
- self._dialog.setHistogram() # Reset histogram if any
- self._dialog.setDataRange(dataMin, dataMax)
- # The histogram should be done in a worker thread
- # hist, bin_edges = numpy.histogram(goodData, bins=256)
- # self._dialog.setHistogram(hist, bin_edges)
-
- self._dialog.setColormap(**colormap)
-
- # Run the dialog listening to colormap change
- self._dialog.sigColormapChanged.connect(self._colormapChanged)
- result = self._dialog.exec_()
- self._dialog.sigColormapChanged.disconnect(self._colormapChanged)
-
- if not result: # Restore the previous colormap
- self._colormapChanged(colormap)
-
- def _colormapChanged(self, colormap):
- # Update default colormap
- self.plot.setDefaultColormap(colormap)
-
- # Update active image colormap
- activeImage = self.plot.getActiveImage()
- if isinstance(activeImage, items.ColormapMixIn):
- activeImage.setColormap(colormap)
-
-
-class KeepAspectRatioAction(PlotAction):
- """QAction controlling aspect ratio on a :class:`.PlotWidget`.
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- # Uses two images for checked/unchecked states
- self._states = {
- False: (icons.getQIcon('shape-circle-solid'),
- "Keep data aspect ratio"),
- True: (icons.getQIcon('shape-ellipse-solid'),
- "Do no keep data aspect ratio")
- }
-
- icon, tooltip = self._states[plot.isKeepDataAspectRatio()]
- super(KeepAspectRatioAction, self).__init__(
- plot,
- icon=icon,
- text='Toggle keep aspect ratio',
- tooltip=tooltip,
- triggered=self._actionTriggered,
- checkable=False,
- parent=parent)
- plot.sigSetKeepDataAspectRatio.connect(
- self._keepDataAspectRatioChanged)
-
- def _keepDataAspectRatioChanged(self, aspectRatio):
- """Handle Plot set keep aspect ratio signal"""
- icon, tooltip = self._states[aspectRatio]
- self.setIcon(icon)
- self.setToolTip(tooltip)
-
- def _actionTriggered(self, checked=False):
- # This will trigger _keepDataAspectRatioChanged
- self.plot.setKeepDataAspectRatio(not self.plot.isKeepDataAspectRatio())
-
-
-class YAxisInvertedAction(PlotAction):
- """QAction controlling Y orientation on a :class:`.PlotWidget`.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- # Uses two images for checked/unchecked states
- self._states = {
- False: (icons.getQIcon('plot-ydown'),
- "Orient Y axis downward"),
- True: (icons.getQIcon('plot-yup'),
- "Orient Y axis upward"),
- }
-
- icon, tooltip = self._states[plot.isYAxisInverted()]
- super(YAxisInvertedAction, self).__init__(
- plot,
- icon=icon,
- text='Invert Y Axis',
- tooltip=tooltip,
- triggered=self._actionTriggered,
- checkable=False,
- parent=parent)
- plot.sigSetYAxisInverted.connect(self._yAxisInvertedChanged)
-
- def _yAxisInvertedChanged(self, inverted):
- """Handle Plot set y axis inverted signal"""
- icon, tooltip = self._states[inverted]
- self.setIcon(icon)
- self.setToolTip(tooltip)
-
- def _actionTriggered(self, checked=False):
- # This will trigger _yAxisInvertedChanged
- self.plot.setYAxisInverted(not self.plot.isYAxisInverted())
-
-
-class SaveAction(PlotAction):
- """QAction for saving Plot content.
-
- It opens a Save as... dialog.
-
- :param plot: :class:`.PlotWidget` instance on which to operate.
- :param parent: See :class:`QAction`.
- """
- # TODO find a way to make the filter list selectable and extensible
-
- SNAPSHOT_FILTER_SVG = 'Plot Snapshot as SVG (*.svg)'
-
- SNAPSHOT_FILTERS = ('Plot Snapshot as PNG (*.png)',
- 'Plot Snapshot as JPEG (*.jpg)',
- SNAPSHOT_FILTER_SVG)
-
- # Dict of curve filters with CSV-like format
- # Using ordered dict to guarantee filters order
- # Note: '%.18e' is numpy.savetxt default format
- CURVE_FILTERS_TXT = OrderedDict((
- ('Curve as Raw ASCII (*.txt)',
- {'fmt': '%.18e', 'delimiter': ' ', 'header': False}),
- ('Curve as ";"-separated CSV (*.csv)',
- {'fmt': '%.18e', 'delimiter': ';', 'header': True}),
- ('Curve as ","-separated CSV (*.csv)',
- {'fmt': '%.18e', 'delimiter': ',', 'header': True}),
- ('Curve as tab-separated CSV (*.csv)',
- {'fmt': '%.18e', 'delimiter': '\t', 'header': True}),
- ('Curve as OMNIC CSV (*.csv)',
- {'fmt': '%.7E', 'delimiter': ',', 'header': False}),
- ('Curve as SpecFile (*.dat)',
- {'fmt': '%.7g', 'delimiter': '', 'header': False})
- ))
-
- CURVE_FILTER_NPY = 'Curve as NumPy binary file (*.npy)'
-
- CURVE_FILTERS = list(CURVE_FILTERS_TXT.keys()) + [CURVE_FILTER_NPY]
-
- ALL_CURVES_FILTERS = ("All curves as SpecFile (*.dat)", )
-
- IMAGE_FILTER_EDF = 'Image data as EDF (*.edf)'
- IMAGE_FILTER_TIFF = 'Image data as TIFF (*.tif)'
- IMAGE_FILTER_NUMPY = 'Image data as NumPy binary file (*.npy)'
- IMAGE_FILTER_ASCII = 'Image data as ASCII (*.dat)'
- IMAGE_FILTER_CSV_COMMA = 'Image data as ,-separated CSV (*.csv)'
- IMAGE_FILTER_CSV_SEMICOLON = 'Image data as ;-separated CSV (*.csv)'
- IMAGE_FILTER_CSV_TAB = 'Image data as tab-separated CSV (*.csv)'
- IMAGE_FILTER_RGB_PNG = 'Image as PNG (*.png)'
- IMAGE_FILTER_RGB_TIFF = 'Image as TIFF (*.tif)'
- IMAGE_FILTERS = (IMAGE_FILTER_EDF,
- IMAGE_FILTER_TIFF,
- IMAGE_FILTER_NUMPY,
- IMAGE_FILTER_ASCII,
- IMAGE_FILTER_CSV_COMMA,
- IMAGE_FILTER_CSV_SEMICOLON,
- IMAGE_FILTER_CSV_TAB,
- IMAGE_FILTER_RGB_PNG,
- IMAGE_FILTER_RGB_TIFF)
-
- def __init__(self, plot, parent=None):
- super(SaveAction, self).__init__(
- plot, icon='document-save', text='Save as...',
- tooltip='Save curve/image/plot snapshot dialog',
- triggered=self._actionTriggered,
- checkable=False, parent=parent)
- self.setShortcut(qt.QKeySequence.Save)
- self.setShortcutContext(qt.Qt.WidgetShortcut)
-
- def _errorMessage(self, informativeText=''):
- """Display an error message."""
- # TODO issue with QMessageBox size fixed and too small
- msg = qt.QMessageBox(self.plot)
- msg.setIcon(qt.QMessageBox.Critical)
- msg.setInformativeText(informativeText + ' ' + str(sys.exc_info()[1]))
- msg.setDetailedText(traceback.format_exc())
- msg.exec_()
-
- def _saveSnapshot(self, filename, nameFilter):
- """Save a snapshot of the :class:`PlotWindow` widget.
-
- :param str filename: The name of the file to write
- :param str nameFilter: The selected name filter
- :return: False if format is not supported or save failed,
- True otherwise.
- """
- if nameFilter == self.SNAPSHOT_FILTER_SVG:
- self.plot.saveGraph(filename, fileFormat='svg')
-
- else:
- if hasattr(qt.QPixmap, "grabWidget"):
- # Qt 4
- pixmap = qt.QPixmap.grabWidget(self.plot.getWidgetHandle())
- else:
- # Qt 5
- pixmap = self.plot.getWidgetHandle().grab()
- if not pixmap.save(filename):
- self._errorMessage()
- return False
- return True
-
- def _saveCurve(self, filename, nameFilter):
- """Save a curve from the plot.
-
- :param str filename: The name of the file to write
- :param str nameFilter: The selected name filter
- :return: False if format is not supported or save failed,
- True otherwise.
- """
- if nameFilter not in self.CURVE_FILTERS:
- return False
-
- # Check if a curve is to be saved
- curve = self.plot.getActiveCurve()
- # before calling _saveCurve, if there is no selected curve, we
- # make sure there is only one curve on the graph
- if curve is None:
- curves = self.plot.getAllCurves()
- if not curves:
- self._errorMessage("No curve to be saved")
- return False
- curve = curves[0]
-
- if nameFilter in self.CURVE_FILTERS_TXT:
- filter_ = self.CURVE_FILTERS_TXT[nameFilter]
- fmt = filter_['fmt']
- csvdelim = filter_['delimiter']
- autoheader = filter_['header']
- else:
- # .npy
- fmt, csvdelim, autoheader = ("", "", False)
-
- # If curve has no associated label, get the default from the plot
- xlabel = curve.getXLabel()
- if xlabel is None:
- xlabel = self.plot.getGraphXLabel()
- ylabel = curve.getYLabel()
- if ylabel is None:
- ylabel = self.plot.getGraphYLabel()
-
- try:
- save1D(filename,
- curve.getXData(copy=False),
- curve.getYData(copy=False),
- xlabel, [ylabel],
- fmt=fmt, csvdelim=csvdelim,
- autoheader=autoheader)
- except IOError:
- self._errorMessage('Save failed\n')
- return False
-
- return True
-
- def _saveCurves(self, filename, nameFilter):
- """Save all curves from the plot.
-
- :param str filename: The name of the file to write
- :param str nameFilter: The selected name filter
- :return: False if format is not supported or save failed,
- True otherwise.
- """
- if nameFilter not in self.ALL_CURVES_FILTERS:
- return False
-
- curves = self.plot.getAllCurves()
- if not curves:
- self._errorMessage("No curves to be saved")
- return False
-
- curve = curves[0]
- scanno = 1
- try:
- specfile = savespec(filename,
- curve.getXData(copy=False),
- curve.getYData(copy=False),
- curve.getXLabel(),
- curve.getYLabel(),
- fmt="%.7g", scan_number=1, mode="w",
- write_file_header=True,
- close_file=False)
- except IOError:
- self._errorMessage('Save failed\n')
- return False
-
- for curve in curves[1:]:
- try:
- scanno += 1
- specfile = savespec(specfile,
- curve.getXData(copy=False),
- curve.getYData(copy=False),
- curve.getXLabel(),
- curve.getYLabel(),
- fmt="%.7g", scan_number=scanno, mode="w",
- write_file_header=False,
- close_file=False)
- except IOError:
- self._errorMessage('Save failed\n')
- return False
- specfile.close()
-
- return True
-
- def _saveImage(self, filename, nameFilter):
- """Save an image from the plot.
-
- :param str filename: The name of the file to write
- :param str nameFilter: The selected name filter
- :return: False if format is not supported or save failed,
- True otherwise.
- """
- if nameFilter not in self.IMAGE_FILTERS:
- return False
-
- image = self.plot.getActiveImage()
- if image is None:
- qt.QMessageBox.warning(
- self.plot, "No Data", "No image to be saved")
- return False
-
- data = image.getData(copy=False)
-
- # TODO Use silx.io for writing files
- if nameFilter == self.IMAGE_FILTER_EDF:
- edfFile = EdfFile(filename, access="w+")
- edfFile.WriteImage({}, data, Append=0)
- return True
-
- elif nameFilter == self.IMAGE_FILTER_TIFF:
- tiffFile = TiffIO(filename, mode='w')
- tiffFile.writeImage(data, software='silx')
- return True
-
- elif nameFilter == self.IMAGE_FILTER_NUMPY:
- try:
- numpy.save(filename, data)
- except IOError:
- self._errorMessage('Save failed\n')
- return False
- return True
-
- elif nameFilter in (self.IMAGE_FILTER_ASCII,
- self.IMAGE_FILTER_CSV_COMMA,
- self.IMAGE_FILTER_CSV_SEMICOLON,
- self.IMAGE_FILTER_CSV_TAB):
- csvdelim, filetype = {
- self.IMAGE_FILTER_ASCII: (' ', 'txt'),
- self.IMAGE_FILTER_CSV_COMMA: (',', 'csv'),
- self.IMAGE_FILTER_CSV_SEMICOLON: (';', 'csv'),
- self.IMAGE_FILTER_CSV_TAB: ('\t', 'csv'),
- }[nameFilter]
-
- height, width = data.shape
- rows, cols = numpy.mgrid[0:height, 0:width]
- try:
- save1D(filename, rows.ravel(), (cols.ravel(), data.ravel()),
- filetype=filetype,
- xlabel='row',
- ylabels=['column', 'value'],
- csvdelim=csvdelim,
- autoheader=True)
-
- except IOError:
- self._errorMessage('Save failed\n')
- return False
- return True
-
- elif nameFilter in (self.IMAGE_FILTER_RGB_PNG,
- self.IMAGE_FILTER_RGB_TIFF):
- # Get displayed image
- rgbaImage = image.getRbgaImageData(copy=False)
- # Convert RGB QImage
- qimage = convertArrayToQImage(rgbaImage[:, :, :3])
-
- if nameFilter == self.IMAGE_FILTER_RGB_PNG:
- fileFormat = 'PNG'
- else:
- fileFormat = 'TIFF'
-
- if qimage.save(filename, fileFormat):
- return True
- else:
- _logger.error('Failed to save image as %s', filename)
- qt.QMessageBox.critical(
- self.parent(),
- 'Save image as',
- 'Failed to save image')
-
- return False
-
- def _actionTriggered(self, checked=False):
- """Handle save action."""
- # Set-up filters
- filters = []
-
- # Add image filters if there is an active image
- if self.plot.getActiveImage() is not None:
- filters.extend(self.IMAGE_FILTERS)
-
- # Add curve filters if there is a curve to save
- if (self.plot.getActiveCurve() is not None or
- len(self.plot.getAllCurves()) == 1):
- filters.extend(self.CURVE_FILTERS)
- if len(self.plot.getAllCurves()) > 1:
- filters.extend(self.ALL_CURVES_FILTERS)
-
- filters.extend(self.SNAPSHOT_FILTERS)
-
- # Create and run File dialog
- dialog = qt.QFileDialog(self.plot)
- dialog.setWindowTitle("Output File Selection")
- dialog.setModal(1)
- dialog.setNameFilters(filters)
-
- dialog.setFileMode(dialog.AnyFile)
- dialog.setAcceptMode(dialog.AcceptSave)
-
- if not dialog.exec_():
- return False
-
- nameFilter = dialog.selectedNameFilter()
- filename = dialog.selectedFiles()[0]
- dialog.close()
-
- # Forces the filename extension to match the chosen filter
- extension = nameFilter.split()[-1][2:-1]
- if (len(filename) <= len(extension) or
- filename[-len(extension):].lower() != extension.lower()):
- filename += extension
-
- # Handle save
- if nameFilter in self.SNAPSHOT_FILTERS:
- return self._saveSnapshot(filename, nameFilter)
- elif nameFilter in self.CURVE_FILTERS:
- return self._saveCurve(filename, nameFilter)
- elif nameFilter in self.ALL_CURVES_FILTERS:
- return self._saveCurves(filename, nameFilter)
- elif nameFilter in self.IMAGE_FILTERS:
- return self._saveImage(filename, nameFilter)
- else:
- _logger.warning('Unsupported file filter: %s', nameFilter)
- return False
-
-
-def _plotAsPNG(plot):
- """Save a :class:`Plot` as PNG and return the payload.
-
- :param plot: The :class:`Plot` to save
- """
- pngFile = BytesIO()
- plot.saveGraph(pngFile, fileFormat='png')
- pngFile.flush()
- pngFile.seek(0)
- data = pngFile.read()
- pngFile.close()
- return data
-
-
-class PrintAction(PlotAction):
- """QAction for printing the plot.
-
- It opens a Print dialog.
-
- Current implementation print a bitmap of the plot area and not vector
- graphics, so printing quality is not great.
-
- :param plot: :class:`.PlotWidget` instance on which to operate.
- :param parent: See :class:`QAction`.
- """
-
- # Share QPrinter instance to propose latest used as default
- _printer = None
-
- def __init__(self, plot, parent=None):
- s